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

The standard sources

  • Time.every interval msg: the metronome. Everything periodic, from a clock widget to a 60fps simulation, is this.
  • Keyboard.onKeyDown / Keyboard.onKeyUp: each delivers a Keyboard.Key (a union: Keyboard.KeyW, Keyboard.ArrowUp, Keyboard.Space, …) wrapped in your message. Note the shape: keys held down are not a subscription concern; you record down and up in your Model and consult it during ticks, which is exactly how the games do it.
  • Gamepad.*: mirrors the keyboard, for controllers.
  • Device.watch: delivers capability changes (can this device hover, is it touch-only), so a page can adapt its controls when an iPad user connects a trackpad. Capabilities, not user-agent sniffing.

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.onKeyDown KeyDown
        , Keyboard.onKeyUp KeyUp
        ]

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.