Episode 4: Applying 'gravity' to the current piece with subscriptions
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:
- The application starts with the state generated by the
init
function that we defined - The
view
function is used to create the initial Html tree that is going to be displayed - Interactive UI elements like buttons generate Messages
- The
update
function calculates a new model based on a message and the old model state 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
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.