loading...

What Is Elm (and a game I'm making with it)

martinsstewart profile image MartinSStewart ・7 min read

What is Elm?

A delightful language for reliable webapps.

-- Elm's official website

I've been using Elm for a little over a year on hobby projects. I started using it out of a desire to be able to create web apps while avoiding the idiosyncrasies in Javascript and its ecosystem.

My experience has been that "A delightful language for reliable webapps" is an honest description. While there is room to improve I've found Elm to be the most pleasant and productive language I have ever used for creating web apps.

Disclaimer: I have only used JS, Elm, and briefly Typescript so I can't comment on other web languages.

In this post I will be explaining what Elm is, why it's good, and how its helped me with a game I've been working on.

Circuit Breaker

The game I'm working on

A brief intro to my game then!

You're the yellow circle (an electron I suppose) and your goal is to evade the white electrons by hopping between adjacent wires while "hacking" the computer chips you come across.

There's also a level editor and some silly dialogue before every level (with inside jokes that only my friends will get).

You can try it here though be aware that it does not work on mobile platforms yet.

Back to Elm

So what is Elm in more detail then?

  • No crashes in production
  • It's a functional language
  • There's no null or undefined
  • Uses static typing but type annotations are optional
  • Apps use a unidirectional dataflow similar to React and Redux

Let's go over some of these points and see how they help in writing apps.

No crashes in production? That's not hard, just put a try-catch around the whole application!

Sure, an application wide try-catch prevents the app from crashing but it just hides the problem and you end up with weird logic bugs instead.

Elm doesn't have exceptions at all. In situations where some kind of error occurs in a function, instead of using throw we just return data that represents that error and let the code calling the function decide what to do with it.

As an example, in Javascript we might do error handling like this

function getAspectRatio(width, height) {
    if (height === 0) {
        throw "Invalid aspect ratio";
    }
    return width / height;
}

// default to 0 if we don't have a valid aspect ratio
var aspectRatio = 0;
try {
    aspectRatio = getAspectRatio(myWidth, myHeight);
}
catch {
}

This has the disadvantage that the programmer might forget to include a try-catch or not realize that a certain function can throw an exception.

The equivalent Elm code looks like this

getAspectRatio width height =
    if height == 0 then
        Err "Invalid aspect ratio"
    else
        Ok (width / height)

aspectRatio = 
    case getAspectRatio myWidth myHeight of
        Ok okValue -> okValue
        Err _ -> 0 -- default to 0 if we don't have a valid aspect ratio

If you're not used to the syntax then it might be hard to follow. The important thing is that there's no throw.

Instead getAspectRatio returns Ok or Err and when we call getAspectRatio the compiler ensures that we handle both cases.

If we forgot the Err _ -> 0 line then the compiler would tell us we made a mistake.

I pasted the example code into my game to get this error. That's why the line numbers start at 400.

Accounting for cases that would otherwise be unhandled runtime exceptions often catches bugs that would end up in production and spares developers the headache of trying to reproduce them from vague bug reports.

For my game this is especially useful. Games are notorious for having many edge cases (and level editors for games, even more so). Having an entire class of bugs not be possible lets me focus on other things.

As an aside, Elm's compiler error messages are often intuitive and helpful. They read more like a spoken sentence and less like cryptic machine noise.

What is a functional language?

There isn't a hard definition on what makes a language "functional" but here are some features that often appear

  • Algebraic data types
  • Pattern matching
  • Immutability
  • Pure functions

These might sound like impractical things, suited more towards academia but they are actually quite useful.

Let have a look at the last two points.

Immutability

Immutability means that once we've created a data structure or assigned a value to a variable, we never change it. Instead, if we want to "change it" we make a copy with that change made to the copy.

Why is that useful? Suppose we want to add an undo button to an app. If we've written our app without immutable state then this is difficult.

Changes that we want to undo will need to have extra code and state in order to know how to undo themselves. It's probably not enough that the data looks the same as it did before, references between different parts of state need to reset to how they were before as well.

This is hard to debug, annoying to test, and easy to break.

In contrast, if our data is immutable, when we make a change we create a copy of the current state and keep a reference to it. When we undo, just swap the new state for the old one.

"That sounds easy to do but breathtakingly inefficient!"

If we are naive about it and made deep copies of our state every time a change is made then yes, this is inefficient.

However, our state is immutable. We know it can't be changed so we don't need to copy everything. Only the part of our state that we want to copy and change needs to be deep copied. The rest can be shallow copied and reused.

In general, immutability makes it easier to understand and debug code. And with Elm, all our data is immutable.

Pure functions

A pure function is a function that is both deterministic and has no side effects.

A function that changes global state, changes the parameters passed to it, makes an HTTP request, etc. has side effects and isn't pure.

A function that can return different values for the same input parameters is non-deterministic and also not pure.

Pure functions are useful because their behavior can be entirely understood in terms of what the function returns for given input parameters. Testing pure functions is a breeze. There's no need to mock various services or worry that the test is going to mess with other tests or make API calls to a production system.

All functions written in Elm are pure. At this scale this also means it's easy embed one program inside another.

I was able to present my game at a meetup by writing a simple powerpoint-like app in Elm and then embedding my game inside it. I could show the game without having to leave the presentation and even include a tacky transition effect (The previous slide made an explosion sound and then fell away to reveal the game. It was great.)

Static typing? More like, excessive typing!

If you've worked with languages like Java, you may have come away with a distaste for statically typed languages. They just make you repeat yourself with things like Person person = new Person(); right?

This is not the case with Elm! You don't need to do any type annotation. The Elm compiler can figure out the type of every variable, parameter, and return value in your entire program (though often it helps to add type annotations for readibility).

This leaves you only with the advantage of static typing, preventing the programmer from mixing up different types and providing better tooling.

In my game this not only catches many simple mistakes I make but also lets me refactor large parts of my codebase without worrying that I'm going to introduce lots of new bugs.

The Elm Architecture (TEA)

The Elm Architecture

-- Borrowed from dennisreimann.de/articles/elm-architecture-overview.html

Almost all apps written in Elm all have an architecture similar to what you'd find in React + Redux applications.

This includes

  • An update function
  • A view function
  • A message type
  • And a model

The model represents the current state of our app. All the data our Elm program can use is contained within it. In Redux we'd call this our "store".

The view takes that model and returns html which the Elm runtime can use to update a virtual DOM.

The message represents all the possible actions that can take place in our app.

Lastly the update takes a message and a model as parameters and returns a new model which the Elm runtime uses as parameter for view to update the DOM.

This architecture is useful for a number of reasons

  • We don't need to concern ourselves with how the DOM gets updated, we just need to write a function that describes what it should look like and leave it to the runtime to efficiently update the actual DOM. I've worked with WPF in the past. Not having to write OnPropertyChanged for every model change saves me a lot of typing and bug hunting.
  • Data flow is unidirectional. This makes it easier to understand why things happen and in what order. When combined with state being stored exclusively in our model, this allows Elm to support time travel debuggers (aka, a tool that lets us hop to past states and view what the DOM looked like then).
  • When everyone writes Elm apps in a similar way, it's easier to understand someone's codebase.

Summary

I've been using Elm for a little over a year. It's been fun and has made me a better programmer in the process.

Worrying less about type errors, missed edge cases, updating the DOM, how to architect my app, etc., make me more motivated and lets me focus on writing new features for my game.

Posted on by:

Discussion

pic
Editor guide
 

Nice post, congrats! Just a small note though. Is not that Elm is similar to Redux, is that Redux was based on Elm, among other technologies, back at the day it was created 😊 redux.js.org/introduction/prior-ar...

 

Thank you, and good point. Interesting to see who inspired who.