In depth overview of Elm and Purescript. Lessons learned porting a game from Purescript to Elm.

TL;DR I’m finally able to read through Purescript/Haskell source code. I ported a game written in Purescript to Elm. You can play with the Elm version of the game right here, what follows are my thoughts on the two languages.

How I got here

A few months ago, I decided I was going to learn Haskell. The first thing I noticed was that I couldn’t read Haskell code. It was so different to anything I had experienced so far. All those strange symbols gave me a very hard time trying to understand what was going on. Maybe I should have started from something simpler. I heard that a language called Purescript existed, that it was purely functional and that it compiled to JS. Sounded like a good deal to me, after all I wanted to experience what it was like to program in a purely functional language, I didn’t want to settle on Haskell if that wasn’t my thing. Plus, a language that compiles to JS has to be approachable, right? Wrong. Purescript was as much confusing and frustrating as Haskell (in fact, they’re very similar languages). So I eventually considered Elm, which turned out to be the perfect language to learn FP.

I didn’t want to give up on Haskell nor Purescript though. Over the coming weeks, I came back multiple times to these seemingly obscure languages. The resources that definitely helped me the most in grokking the concepts behind them are the excellent Purescript By Example and the glorious Haskell Book. I remember looking for short and easy to follow examples while still learning, and the simplest I could find was this little game: star-dodge-clone (link to playable demo). Despite being simple, it wasn’t at all easy, at least for an uninitiated wanderer like me. I remember going through the ~250 total lines of Purescript code but couldn’t figure out pretty much anything.

So I looked at it again a few days ago, and turns out the code does actually make sense!

In order to prove to myself that I was in fact understanding things, I decided to port it to Elm. I think it turned out pretty good.

Note 1 I didn’t bother making it look exactly the same – the mechanics are in place though.

Note 2 The game is pretty old (~1.5 years) but Purescript is pretty much the same language (as far as this game is concerned) and changes to the api are mostly not relevant for the purposes of this post.

Purescript has what the Haskellers want

When you start learning Elm, one of the first things you’ll notice is that some people constantly complain about the lack of type classes. I think they should just look into Purescript and stick with it. The reason being that once you get type classes, there will be some other feature you’re missing, and given that Elm doesn’t aspire in any way to become Haskell, you won’t get that feature. On the other hand, Purescript is very very similar to Haskell, so if that’s what you want, just stick with it.

If you don’t know what type classes are, the language for you is definitely Elm. Learn it and then come back to Haskell or Purescript to see for yourself if there’s actually something you’re missing.

Language simplicity

I find Elm code much easier to reason about and I think that’s because of two reasons:

  • less features means there’s less stuff going on
  • often, there’s only a single way of doing something

That’s not to say that Purescript code is unintelligible (far from it) but there’s a lot more to learn and you must develop an intuition for certain things. The type system in Purescript is more advanced and, as a consequence, you’ll often see involved type signatures. This is not good or bad per se, it’s just something you have to be aware of. On the other hand, the lack of abstractions in Elm is compensated with a ton of boilerplate which you might not like.

Elm also has quite some magic going on behind the scenes. As an example, the original Purescript game defines this (simple) instance for the Direction data type.

data Direction = Up | Down
{-
   The equivalent in Elm is:
   type Direction = Up | Down
 -}

instance eqDirection :: Eq Direction where
  eq Up Up = true
  eq Down Down = true
  eq _ _ = false

There’s two things of note here. The most obvious is that this can’t be expressed in Elm, as it lacks type classes. And in Purescript, if you don’t define the instance, you can’t use the eq operator, meaning that a simple comparison like Up == Down would result in a compiler error.

Surprisingly (or not), in Elm type constructors are comparable by default! So no need for type classes in this case, the magic baked into the language takes care of it.

Application architecture

There is only one way to architect applications in Elm. That’s what made it somewhat famous and what inspired state managers in JS land such as Redux. I find The Elm Architecture (TEA for short) a very very nice and simple pattern to follow when working on UI code. Being Purescript a much more general purpose language, it doesn’t put such constraints on the developer. Of course, there’s no shortage of excellent libraries to structure your frontend code – I played a little with Pux which somewhat resembles TEA.

One of my frustrations in understanding the Purescript code behind the game, was that I couldn’t follow it. In Elm I can be 100% sure that changes to the model are happening in the update function. You develop at a much higher level and you don’t even have to think about certain things.

Contrast this to the first few lines of the main function in the Purescript version of the game.

main = do
  Just canvas <- C.getCanvasElementById "canvas"
  ctx <- C.getContext2D canvas

  inputsRef <- newRef { space: false }

  onSpaceBar (\b -> modifyRef inputsRef (_ { space = b }))

There are so many things happening here, and I can see why I was so confused when reading it.

First of all, there’s do notation. Why are things defined with a <- instead of a =? This is one of the first questions that beginner haskellers face and you’re going to have the same WTF moments in Purescript as well (if you’re unexperienced like I was). Turns out do notation has a very specific purpose and it makes total sense once you figure out how bind works and how annoying it is to nest sequenced computations. (I’ll say it again, if you don’t know what the hell you just read, stick with Elm for a while, you won’t regret it)

Another thing that I thought was not available in purely functional programming languages is mutability. With references you get exactly that (newRef creates a new reference, a variable if you will, which you can update with modifyRef). Of course mutation is controlled and it’s much different than using a mutable variable in an imperative language, but as a beginner it’s hard to tell what’s happening.

I’m convinced these things are not suited for people approaching functional programming, mostly because it can get confusing pretty quickly. If someone was to explain to me this piece of code without getting into the specifics of the Eff monad and how it handles side effects, I would think that I’m just looking at a more complicated way of doing imperative programming. But that’s not it, this code is definitely pure and functional (as is the code you write in Elm), although I think that a beginner has a better chance to learn through Elm’s constraints.

There should be a single way do to things?

I want to have a look at another piece of code, because it shows how much different can be to program in vanilla Purescript compared to TEA.

loop :: Lazy.List Level -> Eff _ Unit
loop levels = do
  clearCanvas
  D.render ctx $ scene (initialState levels)
  onSpaceBarOnce (go (newGame levels))
  where
  go state = do
    t0 <- nowEpochMilliseconds
    inputs <- readRef inputsRef
    requestAnimationFrame do
      t1 <- nowEpochMilliseconds
      case update inputs (t1 - t0) state of
        Right newState -> do
          clearCanvas
          D.render ctx $ scene newState
          go newState
        Left (Tuple now next) -> do
          clearCanvas
          D.render ctx $ scene now
          onSpaceBarOnce (go next)

You wouldn’t be able to write something like this in Elm. Again, there’s a lot of low level stuff going on like clearing the canvas, listening to keyboard events and even direct access to requestAnimationFrame. In general, Purescript is much more low level than Elm which means you can do pretty crazy stuff while being very precise and explicit about what you want to do (without losing purity).

Elm rendering happens inside the requestAnimationFrame loop and there’s no way to escape that. On the other hand, it’s something that the developer doesn’t have to worry about so maybe it’s good. In Elm you listen to events explicitly returning the list of subscriptions you’re interested in, like so:

subscriptions : Model -> Sub msg
subscriptions model =
    Sub.batch
        [ AnimationFrame.diffs Frame
        , Keyboard.downs KeyDown
        , Keyboard.ups KeyUp
        ]

This list gets handed to the runtime and you’ll only going to hear about those events in the update function, there’s no way around this. Again, Elm often enforces a single way to do things and this could be great or very restrictive depending on your taste and experience.

Total and partial functions

A function can be said to be total when you’re handling every possible input. So if you’re taking a Maybe as an input, you must handle both Just a values as well as Nothing values for the function to be total. In Elm, you are forced to do that, while Purescript offers escape hatches, meaning that you can effectively have partial functions. This might seem counter intuitive, why would you deliberately lose the guarantees given by the compiler and the type system? The answer is convenience. Honestly, at first I was surprised to see unsafePartial used that much throughout the Purescript By Example book, because I thought a sane person wouldn’t want to give up totality, but of course that’s not the case.

There is a passage in the book that explains this well. The code sample is very similar to what we’ve seen in the main function above, but makes use of unsafePartial.

main = void $ unsafePartial do
  Just canvas <- getCanvasElementById "canvas"
  ctx <- getContext2D canvas

Note: the call to unsafePartial here is necessary since the pattern match on the result of getCanvasElementById is partial, matching only the Just constructor. For our purposes, this is fine, but in production code, we would probably want to match the Nothing constructor and provide an appropriate error message.

As an example, consider when you’re sure that you’re handling a list with three elements. Surely it would be handy to have a partial version of tail around that returns List a instead of Maybe (List a). After all we are 100% sure that it’s safe to call tail on that list, however the compiler couldn’t prove it at the type level. At times, it can get a bit tedious to handle these situations, for example in Elm you have to write something like this:

list = [1, 2, 3]
result = Maybe.withDefault [] <| List.tail list -- [2, 3]

We will never get the empty list back, but it’s the only way to make the compiler happy and get rid of the Maybe context. In Purescript, you have partial versions of functions at your disposal. Consider this:

import Partial.Unsafe (unsafePartial)
import Data.Array.Partial (head, tail)

list = [1, 2, 3]
result = unsafePartial $ tail list -- [2, 3]

You can see a pattern here, Elm strive to be total and prevent you from shooting yourself in the foot in any way, while Purescript gives you more freedom, but you have to understand the consequences of what you’re doing. A great write up on partiality can be found in the page The Partial type class in the Purescript wiki.

Unsafe operations

In Purescript you can do unsafe stuff which, quoting the docs, means that computing functions “can result in arbitrary side-effects.” Let’s have a look at an example from the game. There is a function used to generate a level, which obviously needs some sort of random input to work. Yet, it has this type signature:

unsafeLevel :: Int -> Level -- Level is just a record

How is that possible?! We pass an Int representing the level to generate and we get out a Level record without any sort of reference to the effects performed? The function is in fact performing side effecting stuff in the body, for example:

entry <- randomRange (50.0 + door / 2.0) (450.0 - door / 2.0)
exit  <- randomRange (50.0 + door / 2.0) (450.0 - door / 2.0)

Where randomRange has type Eff ("random" :: RANDOM | e) Number, so it is certainly effectful. How come this is not present in the type signature of unsafeLevel? Well, with unsafePerformEff you can run an effectful computation. Look at the type:

unsafePerformEff :: forall eff a. Eff eff a -> a

It looks scary, because you should never be able to escape the Eff context, there shouldn’t be any way to go back. The common practice would be to gather of all your effects and hand them to the runtime through the main function, while here we’re effectively bypassing any guarantees about purity and performing undeclared side effects.

Suffice to say this is impossible in Elm. But is it bad as it might look at first glance? Well, no!

One of the most important things I learnt about functional programming is that local and controlled side effects are super useful and don’t undermine purity in any way, as long as functions are referentially transparent. In this case, unsafeLevel clearly is not, so this is certainly not the best example to convey my point, so let’s make another. (I’m sure unsafeLevel was written out of convenience in the game, just to keep it short and sweet).

Think about how easy it is to express some algorithms in terms of imperative code. Or how much performance you could squeeze out of a function if only you were able to express it with dirty old imperative code. Wouldn’t it be great if we could have a tiny local portion of our code that allows mutability? Wouldn’t it be great if a function could use a mutable variable (and maybe even a for loop!) only within its body. After all, if that’s a better way to describe our problem, why not? Recursion and immutability, like anything else, are not silver bullets. Food for thought!

Let’s briefly look at the Elm version, because the problem of generating a game level necessarily touches many more parts of the program. For starters, I created a function generateLevel : Int -> Task Never Level that returns a Task. When run, it will give back a Level record.

In Elm, the difference between Task and Cmd is blurry and it’s not completely clear to me why they both exist, but the runtime expects a Cmd in order to perform effectful computations (getting the current time and use it as a seed for random number generation in this case) so I have to turn my Task into a Cmd like so:

Task.perform LevelCreated (generateLevel level)

This means that, when the computation is done, the application will receive a LevelCreated Level message that will carry the freshly generated level. Going back to square one to what I was saying about application architecture, I can be 100% sure that this (and any other) message is consumed in the update function, so there’s not much guessing to do. Once you understand TEA you should be able to understand any Elm application. The counter argument is that this can be tedious, you have to define a new message, create a command and introduce asynchronicity where none is needed.

Conclusion

This was a very fun and informative experience and I invite everyone to research and learn a bit about these two languages. Both Elm and Purescript are fantastic.

All in all, Elm is much more targeted at beginners. Both the way the language has been designed and the tone of the documentation prove this. However, this is not a bad thing, and I wish people weren’t so quick at labeling it as a toy language. It certainly is not.

Purescript is more featured, more advanced and definitely has a different target audience. Things can get very abstract with this language (same can be said about Haskell) and you have to be prepared for that. I’m just getting started and will study it more in the near future!

Thanks to @giuliocanti and @mfirry for proof reading a draft of this post.

The source code is available at https://runelm.io/c/u4y and on Github. You can reach me on Twitter.