Elm in the wild: A Sandwich Delivery Game

Matthias Putz

New languages often look shiny and cool in their playground examples. But bringing them out into the wild and having to code more than just the examples in the tutorials often reveals the true value of these new languages. So, in order to evaluate the shininess of Elm, a language compiled to JavaScript and used for building web front ends, we decided to use it for a scoped project: We programmed the Sandwich Delivery Game for the “Smart City” exhibition in Basel, Switzerland.

The Sandwich Delivery Game is a game where the player, virtually, has to deliver as many sandwiches as possible to customers while sitting, in reality, on a fixed bike. A cellphone attached to the handlebars in front of the players shows their position on a map. Turning the handlebars changes the orientation of the player on the map. Moving forward is realized with a speed sensor: The sensor is attached to the back wheel of the bike which in turn is fixed in a trainer stand and runs against a resistance. The speed of the wheel is sent to the cellphone. In order to attract attention of passersby, the device’s screen is mirrored to the wall in front of the player. Some impressions of the setup from the exhibition can be seen in image 1 and 2.

Image 1: Setup of bike at the exhibition.
Image 2: Close up view of game in action.

This post will first describe the way from finding the idea of the game, introducing Elm and its winning and loosing points for the project and closes with a description of the overall architecture. The latter shows the challenges of integrating JavaScript libraries in Elm, especially the central map component as well as connecting the inputs of the hardware components to the game logic.

The source code can be viewed on GitHub.

Finding the idea

The requirement was to build something catchy and interactive for the visitors of our exhibition stand at the “Smart City” exhibition in Basel.

At the exhibition we wanted to address the topic of “Connected Mobility”. Therefore we upgraded an ordinary bike with the COBI hardware, i.e. a smart phone holder and a controller for the right hand along with a speed sensor. The phone itself was mounted on the handlebars and should show the game screen. The bike was mounted on a trainer with resistance to the back wheel. With this setup, the player could move faster or slower by using the pedals and breaks as well as move left and right with the moveable handlebars. Letting the player virtually move around the city of Basel was a must have feature (see image 1 and 2). But just moving around, especially if only virtually, is boring. We had to give a task to the player.

After some brainstorming and discarding ideas like a Tron derivate or a platform game (in German we call it jump ‘n’ run) we finally arrived at a delivery game. Because of a tough deadline, the discussion of whether the player should deliver ice cream or sandwiches was pretty short.

A score was introduced to motivate a competition between players: Within a time frame of two minutes the player has to deliver as many sandwiches as possible. With each delivered sandwich CHF 10 are earned. Driving not on a street as well as ignoring red traffic lights are penalized with the realistic Swiss fines of CHF 30 and CHF 60.

In order to make the scenario even more realistic, green waves along with other virtual bikers were added. A column of bikers approaching a red traffic light is triggering the traffic light to change to green. Therefore following a group of virtual bikers may reduce the waiting time on red traffic lights for the player.

The intro screen (opened on a laptop) and a scene from the final game can be seen in image 3 and 4.

Image 3: Intro screen of game.
Image 4: Scene from final game.

Before diving more into our learnings and the architecture, let’s deliver some sandwiches: Sandwich Delivery Game !

How to play with a keyboard or a mobile phone is described on the intro screen depending on which device the game is opened. For the published version, the score server is deactivated since different input methods (keyboard or mobile phone) are not comparable. Moreover, it would be very easy to fake the results.

The bike integration which was done with the COBI DevKit was removed from the code since the DevKit was not released yet at the time the game was developed. Moreover, the Sandwich Delivery Game was the first externally developed COBI module and therefore surely also a test for the now released DevKit.

As a side note: COBI themselves evolved and completed the idea to the Brezelbike.

Introducing and justifying Elm

The Elm Architecture

Elm is a purely functional programming language which has its roots in Haskell: Elm has a similar and clutter-free syntax, is statically typed and has a powerful type inference.

But Elm is far more than just a language, it also provides an architecture which is the most thrilling feature of Elm. Even though the concept of the architecture is old and rusty since it is just Model-Update-View, Elm embodies the concept in such a way that it feels naturally to develop a programm with this architecture.

In Elm the Model-Update-View architecture is realized as follows:

  • The Model defines the state of the application. This can simply be expressed by a type alias:
  type alias Model =
    { time : Time
    , state : State
    , mapInitialized : Bool
    , ...
    }

Anything that may change at runtime has to be described in the Model, otherwise it cannot be updated in the update function.

  • The update function updates the state. For this, the first parameter of update is a Msg: Msg is a union type (or algebraic data type, ADT) of everything that happens in the program and may cause an update to the model:
  type Msg
    = Restart
    | OnMapInitialized
    | Tick Time
    | ...

The second parameter of update is the current Model which should be updated. The result is the updated Model.

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

      OnMapInitialized ->
        { model | mapInitialized = True } ! [ Cmd.none ]

      Tick newTime ->
        ...
      ...

A little bit confusing is that update also returns a command Cmd Msg. A command can be a HTTP request or generating a random number. So, along with the updated model, update can request asynchronous operations, i.e. operations which are no pure functions. The result of these operations are Msgs which get fed into update again by the Elm runtime.

The counterpart of commands, which are not shown in this post, are subscriptions: A subscription Sub Msg also produces Msgs for update. Time or JavaScript callbacks are examples of subscriptions.

For the curious ones: These concepts of commands and subscriptions are described in the “Effects” chapter of the Elm Guide.

  • The third part, View, maps from model to HTML:
  view : Model -> Html Msg
  view model =
    let
      ...
    in
      div [ (Html.Attributes.id "elm-main") ]
        ( [ map
          , mapOverlay
          ]
        )

Again there is a Msg in the return value. So a view can also generate Msgs for the update function, for example when a button is clicked.

Elm’s talkative compiler

Another noteworthy feature of Elm is a surprisingly and incomparably friendly compiler. The compiler not only states compile errors, it also tries to provide a solution and gives hints. One such compiler error is:

ERROR in ./src/elm/Main.elm
Module build failed: Error: Compiler process exited with error Compilation failed
-- TYPE MISMATCH --------------------------------------- ./src/elm/App/Biker.elm

The branches of this `if` produce different types of values.

43|>    if nextRoute then
44|>      case Cons.tail biker.route of
45|>        h :: t ->
46|>          Just { newBiker | route = Cons.cons h t }
47|>        [] ->
48|>          Nothing
49|>    else
50|>      newBiker

The `then` branch has type:

  Maybe
  { id : Int
  , lastLocation : Location
  , location : Location
  , speed : Speed
  , route : Cons Location
  }

But the `else` branch is:

  { id : Int
  , lastLocation : Location
  , location : Location
  , speed : Speed
  , route : Cons Location
  }

Hint: These need to match so that no matter which branch we take, we always get
back the same type of value.

Usually such a long compiler error means something bad has happened (especially when you are coding in C++). But this is not true for Elm. Looking at the error message, the following information is printed:

  • Apparently the compiler complains about the type of an if statement.
  • The problematic if statement is printed: It starts in line 43 of the file Biker.elm.
  • In order to help to fix the bug, the compiler states the inferred and mismatching types of the branches:
  The `then` branch has type:

  Maybe
  { id : Int
  ...

  But the `else` branch is:

  { id : Int
  ...
  • Therefore the only thing to do is to compare and fix the types: The problem is that the if branch returns a Maybe whereas the else branch does not.
  • Consequently, changing line 50 to be Just newBiker solves the problem.
  • Also note the hint at the very end of the compiler error which helps to understand the reported failure.

So the compiler does not only complain about being not satisfied, it also tries to provide information to find a solution and gives hints to improve the understanding of an error that has occurred. Consequently the compiler itself played a not so small role in the on-boarding process by supporting the trainee with a nice error messages.

Upsides of using Elm

Already by means of its architecture, Elm forces the developer to describe the application in a safe way: There is no shortcut for updating the current state but using update. There is also no way to present some other, non-static, information in the view function other than providing it via the model.

Elm also takes over the runtime, the developer does not have to care about it. The developer only provides the update and view function and subscribes to effects like mouse clicks, callbacks from JavaScript or time. Calling the implemented update and view function is done by the Elm runtime whenever it is required.

So in general the main loop is:

  • An update happens.
  • A Msg is sent to update together with the current model.
  • The result is a new model.
  • The new model is put into the view and results in a new view.
  • The new view is shown.
  • Wait for new updates.

On first sight this loop seems to be a restriction for the developer because it is so simple and because it forces the developer to express everything in just these few functions and types. But in the end the loop has all the needed power and additionally prevents the developer from doing (hopefully unknowingly) stupid things like modifying state anywhere or handling concurrency and side effects carelessly.

Important enough to mention: Elm does not render the complete page after each call of the view function. Elm keeps a copy of the current page and computes the difference of the current and the new page. By knowing the differences, Elm updates only these parts of the page. More information on this technique called a “virtual DOM” as well as an Elm-provided performance test can be found in an Elm blog post.

Elm’s architecture and the nature of the language lead to the fact that after satisfying the compiler, which becomes like talking to a friend after some passionate discussions (which the compiler always won), the program worked every time as intended. That was pretty astonishing, we faced no runtime errors (except when we hit a bug in the Elm compiler which in turn was rooted in a JavaScript limitation).

Another advantage arises from the declarative description of the view: animations are almost trivial. By subscribing to the time on every 100ms, the update function gets called every 100ms. An animation then just saves the start time and computes the new position of the object over time.

As a matter of fact, all notifications in the game poping up next to the player arrow are animations. Notifications indicate the remaining time, earned money and penalties. An animation of a notification is a text which moves up and simultaneously gets smaller over time. In order to do this more stylish, i.e. not linear, ease function are used (all available ease functions are on easings.net). With some simplifications, the code is as follows:

type alias Notification =
  { text : String
  , started : Time
  , duration : Time
  }

viewNotification : Float -> String -> Int -> Notification -> Form Msg
viewNotification time text index n =
  let
    lifetime n = (n.started + n.duration - time) / n.duration

    fontSize n = lifetime n |> Ease.outExpo
                            |> (*) 4
                            |> Basics.round
                            |> (+) 12

    paddingTop i n = lifetime n |> Ease.inExpo
                                |> (*) 20
                                |> (-) ((toFloat i) * 20)
                                |> (-) 270
  in
    position (220, paddingTop index n)
             <| rightJustified
             <| fontFamily "Arial"
             <| fontColor n.color
             <| bold
             <| Graphics.Render.text (fontSize n) text
  • The lifetime of a notification describes how long it is already active. Note that a notification with negative lifetime is removed by the update function.
  • fontSize computes the font size based on the lifetime with an outExpo ease function.
  • paddingTop computes the position based on the index of a notification (older notifications are shown above newer ones) and an inExpo ease function with the lifetime.
  • In the in part of the let-in, functions like position, rightJustified etc. from a graphics library are used to create the viewable Form.

Downsides of using Elm

Since Elm is transpiled to JavaScript, i.e. to its own file, it is hard to modularize an Elm application into separate, loosely coupled programs. It is possible to develop independent Elm programs and let them communicate over a JavaScript bridge with subscriptions and commands, but this is certain to become tedious. In general, Elm is intended for the creation of single page application (SPA) and consequently is good at developing exactly this, SPAs.

The Sandwich Delivery Game is a pure SPA and it is only reasonable to program games this way: The game captures the whole page and has to control every single pixel on the screen.

Nevertheless, modularizing within a single application becomes a problem as well: It lets the Model as well as the update function get bigger and bigger with every added feature. At some point the update function is not maintainable and understandable any more and the question on how to split the function will arise.

One solution is to create subtypes in Msg and a handle function for each subtype. Then update acts as a router and delegates the messages to its handle functions.

type Msg = SubMsg1 SubMsg1 | ...
type SubMsg1 = A ... | B ...
...

update msg model =
  case msg of
    SubMsg1 m ->
      handleSubMsg1 model m
    ...

Then, still, every messages enters the update function, but this solution is certainly more expressive and understandable than connecting different modules over a non-visible router.

For our application we just delegated all updates of the 14 Msgs which could not be expressed in one line to specific handle functions and had already a big and easy win regarding comprehensibility.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    Restart ->
      model ! [ restart () ]

    OnMapInitialized ->
      { model | mapInitialized = True } ! [ Cmd.none ]

    Tick newTime ->
      handleTick model newTime

    RoadsResult result ->
      handleRoadsResult model result

    TrafficLightsResult result ->
      handleTrafficLightsResult model result
    ...

Sadly there is currently no support from any IDE for refactorings like extracting code to smaller functions which would be handy for modularizing functions.

Debugging in Elm is different as well and therefore it needs time to get used to it. An advanced approach would be to use the Elm debugger which can be opened in the live view, see images 5 and 6.

Image 5: Debugger in lower right corner.
Image 6: Game with opened debugger and history of messages.

This debugger has some neat features: First of all, the debugger has a history of all messages which have been passed to the update function. Therefore all states of the application can be viewed: Double-clicking on one of the messages shows the state which resulted from the message. This is pretty powerful for analyzing a bug. Also very handy is that the history can be exported and imported in order to share a program run or reconstruct a run later.

On the contrary to our initial expectations the debugger does not stop the program on selecting a message from the history and since we are subscribed on a 100ms basis to time, the messages with the time as content still flow through the update function. Moreover, these time messages have side effects since they draw elements on the map (see the chapter on the game architecture for the flow). Consequently the debugger was, despite analyzing the history of messages, due to the tight interaction with the JavaScript universe, not very useful for us. Nevertheless, the poor man’s debugger of printing messages to the console was a good enough alternative.

Another downside is that on-boarding is hard. People who are not used to functional programming have to wrap their head around handling things in a functional way. For example generating a random number is a side effect. Means: A command has to be returned in the update function and the generated random number will asynchronously be received by the system over the update function via a Msg.

Handling this correctly is painfully at first sight. But then most often the clarity arises within the developer that the problem has to be expressed in this way in order to be free of side effects. A similar and often one of the first experiences is the correct handling of non-existence with Maybe. There is no workaround with null as in Scala. In the end all this leads to learning and using patterns of functional programming.

The Game Architecture

The game consists of a JavaScript and Elm component, interacting with each other to provide the game experience: The core screen of the game is the city center of Basel, presented as a 2D map via Leaflet (the tiles are served by CARTO). Since leaflet is a JavaScript library, the Elm application has to send commands to JavaScript for updating the map and the elements shown on it. On the other side, the JavaScript component listens to the navigation input events, depending on the device: Keyboard inputs, callbacks from the orientation API and/or callbacks from the COBI library. All these callbacks are forwarded to Elm via the subscription mechanism (see the Elm Guide).

The overall architecture with the responsibilities of the JavaScript and the Elm component are described in image 7.

Image 7: Overview of architecture.
  1. Initialization and configuration of the Elm application is done in JavaScript.

  2. The Elm application then initializes the basic layout of the page. After this, the JavaScript part is notified that the map can be drawn.

  3. Following the request from Elm, the lowest layer, the map, is initialized and drawn by JavaScript.

  4. On the top is an overlay layer which is drawn and used by Elm to show the sidebar and the player notifications.

  5. For the navigation, the speed sensor of the bike and the orientation of the cell phone are received via callbacks in JavaScript (alternatively keyboard input is supported in non-mobile browsers). These info are forwarded to Elm.

  6. Due to game play or due to updates from the navigation inputs, Elm updates the displayed map elements as well as the shown map sector and the rotation of the map. The elements of the game are other bikers (blue dots), the player arrow, traffic lights, hot spots for sandwich requests (circles with numbers) and streets.

This architecture was more or less given by the choice to use Elm. Nevertheless it proved to be robust and viable despite the integration of JavaScript, Elm and hardware.

Conclusion

Programming with Elm is fun and can be productive at the same time. This is not only because of the language and the simple but powerful architecture, but also because the power of millions of JavaScript libraries still lie at hand. It is as easy as it can be to integrate JavaScript into the holy world of the Elm universe. Developing the game proved the concepts of Elm to be well thought-out and that its way of integrating JavaScript may be an enabler for a lot of programs written in Elm.

On the other hand, Elm is very young. This not only means that breaking changes between releases still happen but also that things like tools for refactoring are still missing. These are rather high-level but nonetheless non-negligible problems of the new “product” Elm.

In conclusion, use Elm in every case where it is meaningful. In every other case, try to mimic the architecture provided by Elm in order to have a stress-free life.

Thumb matthiasputz profilbild

Matthias Putz ist Consultant bei der innoQ Schweiz GmbH. Er beschäftigt sich dabei vorrangig mit der Implementierung von Backend-Systemen im IoT Umfeld z.B. durch den Aufbau einer Plattform für Vernetzte Mobilität. Zuvor war er als Embedded-Softwareentwickler in der Automobilbranche tätig.

Weitere Inhalte

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen