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 aKeyboard.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:
| Piece | Type | Role |
|---|---|---|
Model | your record | all state |
Msg | your union | all events |
init | (Model, Cmd Msg) | starting state and effects |
update | Msg -> Model -> (Model, Cmd Msg) | all behavior |
view | Model -> View Msg | all appearance |
subscriptions | Model -> Sub Msg | all 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.