Adventures in Elm, Part I

This is the first post of a two-part series exploring the Elm programming language. This post will be an introductory overview of the language, tooling around it and development workflow. In the next installment, I plan to attempt to build a moderately complex app using Elm. This post was featured on the Track JS blog.

Soylent Elm is Made out of Haskell!!

Elm's homepage touts itself as "a delightful language for building reliable webapps," promising great performance without runtime exceptions. The nearly pure functional language takes most of its syntax and concepts from Haskell and ML, and it aims to marry the best of functional programming with HTML, CSS and JavaScript interoperability and a strong focus on being user friendly and producing clean, concise, expressive code with minimal tooling. Think of it like the Python of pure functional languages. Elm is statically typed via a Hindley-Milner type system and compiles to JavaScript.

Elm takes a rather blatant preference for functional programming. From the docs:

Why a functional language?

Forget what you have heard about functional programming. Fancy words, weird ideas, bad tooling. Barf. Elm is about: * No runtime errors in practice. No null. No undefined is not a function. * Friendly error messages that help you add features more quickly. * Well-architected code that stays well-architected as your app grows. * Automatically enforced semantic versioning for all Elm packages.

Redux, I am Your Father

The Elm architecture inspired the Redux JavaScript library. This pattern has been gaining notice recently as "JavaScript fatigue" reaches an all time high, with more JavaScript developers willing to give non-JavaScript solutions a chance. Since Elm inspired Redux, mandates immutability and declarative programming, and has its own virtual DOM implementation, maybe it isn't a far cry for those of us who have used the React ecosystem.

Let’s explore what it’s like to work with Elm. How do we handle errors? How do we debug? Is it as beginner-friendly as it claims?

Setting Up

First, I need to get everything installed. After running the Mac installer, I have all the Elm goodies on my PATH: elm-repl, elm-reactor, elm-make and elm-package. That took all of 3 minutes. No hiccups so far. Following the Elm instructions, I next installed the "elm" extension for VS Code. After installing, I can create an Elm file and get nice syntax highlighting, as shown in their example gif:

I also installed the elm-format extension. So far so good! The remainder of the docs cover the Elm language and Elm architecture. I read through these fairly brief overviews, but I won't rehash them here. Let's try to get a simple app running.

Hello, Elm

On the command line:

$ mkdir hello-elm
$ cd hello-elm
$ elm-package install elm-lang/html
Some new packages are needed. Here is the upgrade plan.

  Install:
    elm-lang/core 5.0.0
    elm-lang/html 2.0.0
    elm-lang/virtual-dom 2.0.3

Do you approve of this plan? [Y/n] Y  
Starting downloads...

  ● elm-lang/virtual-dom 2.0.3
  ● elm-lang/html 2.0.0
  ● elm-lang/core 5.0.0

Packages configured successfully!  

Per the docs, elm-reactor is a way to "get a project going quickly." We should be able to create a Main.elm file, put some code in it, and run elm-reactor in order to build and run the file in the browser. Let's try that. I opened the "hello-elm" directory we just created in VS Code, and I see an "elm-stuff" directory and an "elm-package.json" file. Now I'll create a file called "Main.elm" (following Elm file name conventions, the entry module is called "Main" and all modules are capitalized). I've added these lines in the file as an initial test:

import Html exposing (..)

main =  
  div [] [ text "Hello, Elm!" ]

Now to run it via elm-reactor. On the command line in our project directory:

$ elm-reactor
elm-reactor 0.18.0  
Listening on http://localhost:8000  

Now we just go to localhost:8000 and click the "Main.elm" link to build and run our module.

Success! So far there's been absolutely no fuss following the setup and first app instructions.

Using the Elm architecture

Now let's try the canonical counter example given in the docs. Replace the contents of Main.elm with the following:

import Html exposing (Html, button, div, text)  
import Html.Events exposing (onClick)

main =  
  Html.beginnerProgram { model = 0, view = view, update = update }

type Msg = Increment | Decrement

update msg model =  
  case msg of
    Increment ->
      model + 1
    Decrement ->
      model - 1

view model =  
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

It works!

We now have a working example of the Elm architecture.

Compilation

So far I've been impressed with the out of the box features and simplicity of Elm's tooling, but how does Elm handle compiler errors? Can we sneak anything past the compiler? Let's give that a shot. I'll make an intentional mistake in Main.elm and try to use something that is undefined:

    Increment ->
      model + 1 + x

First off, notice that the VS Code Elm extension catches this. Awesome! Now let's see what elm-reactor has to say about it:

Not only is Elm refusing to compile this, it gives us some suggestions for a possible fix. The error messaging overall is pretty useful. This is a much needed departure from the cryptically academic error messages of the older functional languages which inspired Elm.

I tried this:

update msg model =  
  case msg of
    Increment ->
      model + 1
    Decrement ->
      model.fake - 1

and VS Code Elm extension told me "model does not have a field named 'fake'." After working in only JavaScript for the past two years, this seems pretty cool! Lo and behold, the same output is also shown in elm-reactor when we try to run the code:

Well, the compiler foiled us at every turn, but one way to crash Elm at runtime is to do so on purpose with the tool they created for this job!

import Html exposing (Html, button, div, text)  
import Html.Events exposing (onClick)

main =  
  Html.beginnerProgram { model = 0, view = view, update = update }

view model =  
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    , div [] [ button [ onClick Crash ] [text "Crash"]]
    ]

type Msg = Increment | Decrement | Crash 

update msg model =  
  case msg of
    Increment ->
      model + 1
    Decrement ->
      model - 1
    Crash -> 
      Debug.crash "Pow!"

This renders the app unresponsive and gives us an uncaught JavaScript runtime exception:

Debugging Elm

The Elm compiler attempts to remove logical errors in your code, but the app is still going to run on the web (“the most hostile software engineering environment imaginable”). Errors are going to happen from timing failures, incompatible browsers, and invasive plugins. We’re not going to get very far without monitoring and debugging. So how does Elm's debugger work? Can I trace variables? Can I set breakpoints and inspect? Much to my surprise and disappointment, the official guide has no information about debugging!

However, I did notice when running Elm files via reactor that there's a built in debugger attached.

This feels very similar to Redux devtools (reverse and replay actions, see the app state after any given action, etc), but, despite its benefits, this tool doesn't meet my expectations for overall debuggability.

I found very few trustworthy and up to date resources on Elm debugging. I tried piecing together what scant information I could dig up via google, but all the sources seemed to conflict. For example, most of the debugger functions enumerated here don't seem to be present in the version of Elm I have installed (0.18.0, the latest official release at the time of writing). Luckily, VS Code saved me.

So it appears that Debug only has two methods, crash and log, one of which we've already used. Let's try the other one. Gleaning what I can from the method signature, I tried using log like this:

    Increment ->
      Debug.log "model" model + 1

I'm inferring that Debug.log takes one string argument to use in the log output, one argument of any type of value, and then returns the second argument so that the debug expression evaluates to that argument (is replaced by it) making it a no-op in relation to the surrounding code. Let's see if elm-reactor agrees with me:

It works! Cool.

I’ve been able to fairly easily figure out how to use the native Debug module in Elm thanks VS Code’s Elm integration. However, the lack of documentation and apparent instability of the approach to debugging in Elm is the only pain point I've come across so far.

Tradeoffs of Compilation

Since we're leaving everything to the compiler, we get no sourcemaps for production debugging. Sourcemaps have been brought up as a feature request for Elm, but they have all been denied per the argument that you only need sourcemaps if you expect runtime exceptions in the compiler output. If an error happens, it's considered a bug in the compiler and treated as a top priority. This also means the Elm development work cycle is different than the JavaScript one. Instead of the process being: write code, debug in the browser, modify code, repeat; our process is: write code, compile, modify code per any compiler errors, repeat.

Compiler errors are surprisingly useful in Elm, however, if for any reason we did need to debug the user experience of our app, we would be stranded without sourcemaps. In this sense, using Elm instead of JavaScript means trading runtime debuggability for the compiler’s guarantee of no runtime exceptions.

However, this is the web, so debugging continues to be absolutely necessary. Luckily, debugging Elm’s compiled JavaScript at runtime is fairly straightforward:

Debugging is also a desirable tool for development in Elm when our code compiles, but does not behave as expected. For example, bottomless recursion will not cause a compiler error:

import Html exposing (Html, button, div, text)  
import Html.Events exposing (onClick)

main =  
  Html.beginnerProgram { model = 0, view = view, update = update }

view model =  
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

type Msg = Increment | Decrement

update msg model =  
  case msg of
    Increment ->
      update msg model

This will cause the JavaScript thread to lock up, making the app unresponsive.

Where Do We Go From Here?

In this post, we became acquainted with the reasoning behind Elm, used its built-in tools and development flow, tried out the Elm architecture, built a trivial frontend web application in Elm, and learned to do some basic monitoring and debugging. Stay tuned for the next post, in which we will try to build something non-trivial using Elm!

comments powered by Disqus