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.