Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Subscriptions: Listening to the World

Commands are outbound: your code asks, the runtime does. But some information is not asked for; it streams. The clock ticks whether or not anyone fetched it. Keys go down when the player decides, not when your code polls. Subscriptions are MVU’s inbound channel: standing declarations of interest that turn outside events into messages.

Declared, not registered

Every page carries a subscriptions field: a pure function from the current Model to what the page wants to hear about.

subscriptions : Model -> Sub Msg
subscriptions model =
    if model.running then
        Time.every (Time.millis 40) Tick
    else
        Sub.none

Read it as a sentence: “while the model says we are running, deliver a Tick every 40 milliseconds; otherwise, nothing.” The runtime calls this function after every update and reconciles reality to it, starting timers that should exist and stopping ones that no longer should.

Compare this to addEventListener and setInterval. Imperative listeners are registrations with a lifetime you manage: attach on mount, detach on unmount, remember the handle, do not double-attach, do not leak on that one early-exit path. The genre of bug is famous (the interval that keeps firing on a page the user left three screens ago). A Mar subscription has no lifetime to manage because it is not a registration at all; it is a description of the desired present, recomputed from state. Pause the game by setting running = False in the Model, and the timer stops, because the description now says Sub.none. There is nothing to clean up; there never was a resource in your hands.

The building blocks compose like everything else:

Sub.none  : Sub msg
Sub.batch : List (Sub msg) -> Sub msg

Two kinds of news: occurrences and state

Not everything the world reports has the same shape, and the difference runs through Mar’s whole input surface. Some news is an occurrence — a moment that matters because it happened: a tick elapsed, a pointer tapped, a wheel notched. A beat later, with nothing changed, there is nothing left to read; the moment was the whole of it. Other news is state — a fact that persists and that you sample: which keys are held right now, where each finger rests, how far a stick is pushed, how big the canvas is. A beat later, untouched, it is still true and still worth reading.

That thought experiment is the whole test: frozen a beat later, is there still a value to read? Yes means state; no means occurrence. Mar spells the two apart. Occurrences keep the on* and every verbs and hand you the moment. State arrives as a mirror: a watch subscription that hands you the entire current snapshot and re-sends it whenever it changes, so your Model can simply hold it. You never rebuild held state from a stream of edges — the runtime keeps the score.

The standard sources

  • Time.every interval msg: the metronome, and the one pure occurrence in this list. Everything periodic, from a clock widget to a 60fps simulation, is this.
  • Keyboard.watch msg: the held-key mirror. It delivers { down : List Keyboard.Key } — every key held at this instant, in press order (Keyboard.KeyW, Keyboard.ArrowUp, Keyboard.Space, …) — and re-delivers the whole set the moment it changes. There is no down/up event pair, because held keys are state, and state arrives as state. “This key just went down” is a question you answer by diffing the new set against the one you stored last frame.
  • Gamepad.watch msg: the controller mirror, same idea. One snapshot carries whether a pad is connected, both analog sticks (each axis -100..100), and the held buttons.
  • Device.watch msg: the capabilities mirror — can this device hover, is it touch-only — so a page adapts its controls when an iPad user connects a trackpad. Capabilities, not user-agent sniffing.

Three of those four are mirrors, and they share one contract worth stating once:

  1. Seed on subscribe. The instant you subscribe, the mirror fires with the current snapshot, so your Model starts correct, not empty.
  2. Whole snapshots, never deltas. Every message is the complete truth of its domain — no ordering hazard, nothing to miss. Store it verbatim: KeysChanged st -> ( { model | keys = st.down }, Cmd.none ).
  3. Change-driven. Identical state emits nothing, which is why OS key auto-repeat — the same key, still down — stays silent instead of machine-gunning your update.
  4. Self-healing. Alt-tab away mid-keypress and the runtime empties the held set on window blur; a cancelled touch drops from the pointer list; an unplugged pad zeroes out. A key can never get stuck down, because your app never keeps the ledger that could go stale.

That last point is the quiet win. Before mirrors, every canvas game hand-rolled a key ledger, and every one shipped the same bug: alt-tab mid-jump, the keyup never arrives, and the character runs at the wall forever. The runtime cannot heal a ledger it cannot see — so the ledger moves into the runtime, and the bug is fixed once, for everyone.

Ticks are how games happen

A detail with big consequences: a game in Mar is not a special program with a render loop. It is an ordinary MVU page whose subscriptions asks for a fast Tick, whose update advances a simulation one step per tick, and whose view draws the Model onto a canvas.

subscriptions model =
    Sub.batch
        [ Time.every (Time.millis 16) Tick
        , Keyboard.watch KeysChanged
        , Gamepad.watch PadChanged
        ]

The runtime aligns these fast timers with the display (and, when a device falls behind, runs catch-up steps so game speed stays true to wall-clock time). But architecturally nothing new happened: state is still one value, ticks are still messages, update is still pure. That purity is why a Mar game gets save-anywhere, replay, and time-scrubbing debugging for free: a game state is just a Model, and a session is just its message list.

The complete frontend contract

With subscriptions in place, the frontend picture is finished, and it is pleasingly small. A page is five declarations:

PieceTypeRole
Modelyour recordall state
Msgyour unionall events
init(Model, Cmd Msg)starting state and effects
updateMsg -> Model -> (Model, Cmd Msg)all behavior
viewModel -> View Msgall appearance
subscriptionsModel -> Sub Msgall standing interests

Six, counting the routing record that binds them. There is no seventh thing. Every Mar frontend you will ever read, from a sign-in form to a real-time strategy game, is an instance of this table, and once that clicks, unfamiliar codebases stop being unfamiliar.

Next: how pages assemble into an application, and how navigation and sessions fit the same mold.