Functional Reactive Game Programming

Elm is a purely functional language designed for web frontend development. It promises a better developer experience than Javascript thanks to its powerful type system and compile-time type checking. Elm comes with a library for reactive user interface programming, the paradigm made (very) popular by React.js.

I first learned about Elm at a meetup hosted by my employer, and was intrigued right away. Functional languages have had a strange attraction on me ever since I was taught Haskell in university, but I had never since found the opportunity to reconnect with them after graduation. So I decided to do a little project in Elm to find out how well the functional paradigm works in the world of web UIs.

But what kind of project could that be? Something that does not need a backend would be nice, so I could focus completely on Elm, and something cool and fun of course – and what is cooler and more fun than games? So I chose to make a clone of one of the greatest video game classics of all time: Tetris. (At that time I hadn’t seen that one of the demo apps featured on the Elm website is also a Tetris clone.)

The game is playable now, if still a little rough around the edges – you can try it out here. So I want to put down my summary of what getting into Elm felt like, and how well I thought it supported my development process.

Note: All code snippets are Elm v0.17. By now, 0.18 is out and some things have already changed, so my code won’t run without modifications in the current version.

What worked well

Reactive user input handling

Elm’s subscription model is a natural fit for game programming. It works by associating a message type with an event, such as a key press or a timer tick. Whenever that event occurs, a message of the given type is passed to the app’s update function, where it can be handled like any other message. Here’s how easy it is to listen to keyDown events and hook into the AnimationFrame API:

type Msg = KeyDown Int | Tick Float

subscriptions: Model -> Sub Msg
subscriptions model = Sub.batch [
    Keyboard.downs KeyDown,
    AnimationFrame.diffs Tick
    ]

main: Program Flags
main = App.program {
    ...,
    subscriptions = subscriptions
    }

Declarative graphics

Elm comes with a graphics package that offers a nice functional API on top of HTML5’s canvas element. Rendering a frame is as simple as creating a regular HTML element. For instance, Elm-Tris’s Game Over screen is created essentially by this code, rendering a semi-transparent black rectangle over the entire playing field and a big red message in the middle:

let elems = [
        rect width height
            |> filled black
            |> alpha 0.5
            |> moveX centerX,
        fromString "Game Over"
            |> typeface ["Helvetica", "Arial", "Sans-Serif"]
            |> height 40
            |> color red
            |> text
            |> moveX centerX,
        -- more elements
        ]
in  div
        []
        [ collage width height elems |> toHtml ]

Since every frame is built from scratch, I never had to worry about what the previous one had looked like and what to do with it – the same principle that got me excited for React. Only one aspect of this library caused me some headaches, as I detail further below.

Functional game logic

Functional languages are traditionally good at processing lists, so if I can express the state of my game in terms of lists, I should be good to go, right? Well, what is a Tetris block other than a list of four squares? And what is that heap of already placed blocks on the field other than another – potentially very long – list of squares? Using this kind of representation as a starting point, I was able to express the game’s rules in a concise and (mostly) straightforward way. The code certainly isn’t as clean and well-organized as it could be, but even the larger functions feel quite understandable to me.

And though I did tear my hair at times over compiler errors when they didn’t immediately seem to make sense to me, I suppose I’d have suffered far more if I had been working in an interpreted language that let my inevitable mistakes slip through unchecked.

What took some time to get used to

Getting data out of the application

I wanted to store the player’s high score in a cookie to be able to display it during future playing sessions. However, Elm can’t access cookies directly; that would be a side effect and thus a violation of its pure functional nature. Instead, I had to use a port to pass the cookie data out of the app. Ports can be either sending or listening, depending on the direction of data through the application boundary. Here I declared a sending port that I could then send data through by using it like a regular command:

port highScore: Int -> Cmd msg

-- somewhere in my update logic
let nextModel = calculateNewGameState
    command = if nextModel.status == GameOver
    then highScore nextModel.highScore
    else Cmd.none
in  (nextModel, command)

The JavaScript code that starts the app can subscribe to that port and handle the data that is passed through it:

app.ports.highScore.subscribe(function(score) {
  Cookies.set('highScore', score.toString());
});

It is quite a smooth and elegant mechanism, after all – once you get the hang of it.

Getting data into the application

Having successfully stored the high score, I then needed to pass it back to the application on subsequent startups. Again, Elm is unable to actively read outside data. A listening port may have done the trick, but for this use case there is an even simpler way: flags. Flags are essentially arguments that are passed into the app as it is started:

var app = Elm.Main.embed(element, {
  highScore: parseInt(Cookies.get('highScore')) || 0,
});

Inside Elm, flags are passed to the init function. To make this work, the main function needs to use App.programWithFlags instead of the regular App.program:

type alias Flags = {
    highScore: Int
}

init: Flags -> (Model, Cmd Msg)
init flags = (
    {
        ...,
        highScore: flags.highScore
    },
    Cmd.none)

main: Program Flags
main = App.programWithFlags {
    init = init,
    ...
}

Limited (but improving) editor support

I wrote Elm-Tris in Atom using the language-elm package. It offers syntax highlighting, but not much more. For an IDE addict like myself who works with PhpStorm every day, that was disappointing, especially when the language’s type system would be a prime target for all kinds of code assistance and static analysis.

Fortunately, there is now an Elm language plugin for IntelliJ that looks quite promising, so I would definitely use that for my next project.

What made things complicated

No console.log()

One of the most convenient features of Javascript is that when your program misbehaves and you’re not sure why, you can always just dump any data to the console. Not so in Elm – obviously, because that would be a side effect. I found myself wishing for this ability quite a few times, and I’m sure it would have saved me more than a bit of frustration, but the language design simply does not allow for it.

Elm now has a debugger that looks really cool; sadly, I only learned about it when the game was already done.

The coordinate system of elm-graphics

As much as I praised elm-graphics earlier, it has one feature that I did not appreciate: The origin point (0/0) of its coordinate system is located in the center of the canvas. I’m sure that is extremely convenient when plotting mathematical functions, but for Tetris I would sure have preferred to have the origin in the upper left corner as usual. Especially since the game does not just draw the playing field on the canvas, but also a sidebar with stats, which meant that the center of the canvas is different from the center of the playing field. Having a more traditional coordinate system would have saved me some needlessly complicated translations (and several errors).

Randomness

In most cases, the deterministic nature of pure functions is a good thing, but in games especially, we sometimes want non-determinism. Tetris would be much more boring if the sequence of falling blocks was the same every time we played the game.

Being purely functional, Elm can’t do randomness the way mainstream languages do it. Here, the random number generator requires a seed to operate. But the initial seed should itself be a random value, otherwise the generated numbers would not be very random! It’s a bit of a catch-22 that can only be solved by inserting a seed from the outside. Fortunately, using flags as above this isn’t hard:

var app = Elm.Main.embed(element, {
  random1: Math.round(Math.random() * 100000000),
  random2: Math.round(Math.random() * 100000000)
});

Inside Elm, these two random values can be turned into a seed. Attaching the seed to the model ensures it is available everywhere. Finally, generating a random number also returns a new seed that we have to replace the old one with. For instance, if we want to simulate the roll of a dice:

init flags = (
    {
        ...,
        randomSeed = Random.initialSeed2 flags.random1 flags.random2
    },
    Cmd.none)

d6: Model -> (Int, Model)
d6 model =
    let
        {- Random.int does not generate a value, even though you might think it does.
        It merely creates a generator which then has to be called. -}
        generator = Random.int 1 6
        (roll, newSeed) = Random.step generator model.randomSeed
    in  (roll, { model | randomSeed = newSeed })

This feels very clumsy compared to just calling Math.random() or whatever your favorite non-pure language offers.

Conclusion

Getting back into pure functional programming after all these years was a (mostly) pleasant experience, and I was positively surprised how well Elm lends itself to areas of programming that aren’t typically associated with this kind of language. Whether it is going to have a future outside of hobby projects remains to be seen, and I have a hunch that it will be very hard to gain wider adoption, not just due to the unfamiliarity of most developers with the language’s principles, but also due to how large and dominant the Javascript ecosystem is. As it stands, I expect it to become the web’s Haskell: a “cool” language that only one or two companies use productively and that otherwise lives only in a small community of die-hard fans and the occasional research project like this one. But I’d sure like to be proven wrong.

Share