axelerator.de

Episode 4: Applying 'gravity' to the current piece with subscriptions

4 minutes
#elm
24 July 2021

After the marathon episode last week I kept it to more digestible 30 minutes this time.

The goal was to have the current piece drop by one row every second.

I added the necessary types and property to the Model to store the position, but more interestingly I started to use ‘subscriptions’ to updated it every second.

The changes made in this episode are available on Github: Branch Episode4 Commit

To trigger an event every second in Elm we have to understand how subscriptions work in Elm. They have been a part of our application definition from the beginning. But we got away with ignoring them because they were empty (Sub.none)

main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none

To understand how they fit with the elements we already know, let’s revisit the system as far as we know it:

Elm application loop

  1. The application starts with the state generated by the init function that we defined
  2. The view function is used to create the initial Html tree that is going to be displayed
  3. Interactive UI elements like buttons generate Messages
  4. The update function calculates a new model based on a message and the old model state
  5. GOTO 2

The beauty of this model is its simplicity. With the update function there is exactly one place in every Elm app where the application state is altered/calculated. This makes it straight forward to understand and alter different Elm applications. Vanilla JavaScript applications on the other have no restrictions on who changes what whatsoever.

The main challenge for such restricted systems is to still be able to write every possible application. One scenario that can not be expressed in the init->view->update model we’ve expressed so far is the one we want to look at now:

Every second the position of the current Tetris piece should be lowered by one row

In vanilla JavaScript we would use the setInterval function that comes with the browser

But Elm deliberately forbids calling JavaScript functions directly. One reason for that is, that we want to maintain the rule that the model is only ever ‘altered’ from the update function. So even if we could register something with setInterval we would not be able to modify the model directly.

So since we know we want to modify the position, so ultimately the model we know have to add a new branch to our update function. For that we extend our message type Msg with a new variant GravityTick. Now we can handle that new variant in our update function to calculate a new model state with the update piece position:

type Msg = ... | GravityTick 


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GravityTick ->
            ( dropCurrentPiece model
            , Cmd.none
            )

dropCurrentPiece : Model -> Model
dropCurrentPiece model = ...

So far so good, but how do we produce this new message? So far we only learned how to produce messages from interactive elements like <button>.

That’s the moment where subscriptions come into play! With the subscriptions function that’s part of our application definition we can register sources of messages that are not triggered by the user directly.

main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

subscriptions : Model -> Sub Msg
subscriptions _ =
    Time.every 1000 GravityTick

Elm application loop

With the every function that ships with the Elm core library we can generate such a source.

Time.every gets called with a number of milliseconds that defines how often we want the message to be generated. The second parameter specifies which message will be sent.

But specifically the second parameter in the signature of every looks a bit more complicated than what I’ve described.

every : Float -> (Posix -> msg) -> Sub msg

The second parameter is of type (Posix -> msg) and that’s definitely not just a message! That observation is correct and the reason for that is that every wants to pass on a bit of information along with our message: the current, absolute time.

The reason for that is that the browser cannot guarantee that the message is actually sent exactly every second. So for applications that want to track the passed time it is more precise to calculate that based on the timestamp that gets passed along.

For every to be able to pass the time with our message it needs to be passed a function that expects a time (Posix) and returns a message.

If we add Posix as parameter to our variant definition, the name of the variant serves as constructor of this variant. And its signature is exactly the one that Time.every asks for. That’s why we can just write Time.every 1000 GravityTick in this more complete example:

main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

type Msg = .. | .. | GravityTick Posix | ..

subscriptions : Model -> Sub Msg
subscriptions _ =
    Time.every 1000 GravityTick

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GravityTick currentTime ->
            ( computeNewModel currentTime
            , Cmd.none
            )
        ...

For our Tetris however we’re not really interested in how much time elapsed, so you wont find it used in our actual code.