axelerator.de

Episode 11: Game Over

3 minutes
#elm
23 September 2021

There are still a few elements missing to be able to call our Tetris complete. However, with the changes from episode 11 (40min) weā€™re at least able to tell the player ā€œGame Overā€.

The last commit on the episode11 branch reflects the changes I made during the recording.

This time I didnā€™t really use any new, fancy concepts. But this is another feature that differentiates Elm from many other programming languages. The Syntax is comparatively simple. In other languages, like Ruby or Python for example, we find syntax elements for many dogmas (object oriented and functional). As a result, there are naturally multiple ways to approach a problem, none of which is necessarily more dogmatic to the language than the other. The fact that Elm is dedicated to the functional approach only leads to fewer diverging ways to solve a particular problem. This leads to more unified code which helps to understand code that Iā€™ve not written myself faster. And conversely also to write code that other people understand faster.

Of course, there is still enough room to express things a bit differently, even in Elm. A tool that I often only think of on the second attempt is pattern matching. But I did manage to think of it for this weekā€™s changes eventually.

The most important change however was the ā€˜upgradeā€™ of our central Model type from an alias to an algebraic data type.

before:

type alias Model =
  { board : Board
  , currentPiece : Maybe CurrentPiece
  }

after:

type Model =
    = RunningGame GameDetails
    | GameOver Board

Even though the ā€œcontentā€ of the two variants is nearly the same it pays off to introduce a clear distinction between the two game states now. A variety of operations doesnā€™t make sense to apply when the game has ended. By expressing that state in its own proper variant we can let the compiler direct us to the places in the code where we should check if the logic still adds up for our new case.

One central place for that is the update function. As mentioned in the stream I learned the following trick from Richard Feldman. He maintains the Elm SPA example, a fully-fledged fullstack application built with Elm, which contains lots of useful patterns on how to deal with real-world problems.

I changed our update function from

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

to look like this (abbreviated):

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case ( msg, model ) of
    ( GravityTick _, RunningGame gameDetails ) ->
      dropCurrentPiece gameDetails

By extending the expression between case ... of to a tuple of msg and model we can now also match on the state of our model. We also add a ā€œfallthroughā€ branch that gets matched for all the combinations we didnā€™t explicitly name. That has the pleasant effect that we donā€™t need to specify the combinations that donā€™t make sense, for example (KeyDown key, GameOver)

That means in the end we need less code, which is usually desirable. But it also comes with one drawback. We lose the luxury of the compiler being able to point us to the update function whenever we add a new variant to our Msg type. In the edited version of our update function we now have the _ -> ... branch that will also match any new variant we match. So in the end one has to balance whatā€™s more important on a case-by-case basis.

  • How many cases do I have to be explicit if I donā€™t want to add the fall through?
  • How easy is it to find out where I need to extend case expressions if I add a new variant?