The Elm Language

MC706.io

I recently was rewatching a video on Immutable User Interfaces introducing GraphQL and its interop with React/Redux, when the speaker did a head nod to the Elm programming language. The first few times I had seen the video I was so enamored by GraphQL that I had completely missed the Elm reference. But this time, I decided to dig down the rabbit hole, and here are my findings so far.

What is Elm?

This is the first question I went looking to answer. There is not a whole lot out there on it, and there are few very zealous evangelists of it out there, so I watched some youtube videos and dug through some tutorials.

Elm is a Strongly Typed, Purely Functional programming language that compiles to javascript and offers some lucrative features and guarantees. Unlike ReactJS or AngularJS, it is not a framework. Unlike Typescript, it is 100% typed, and purely functional. Syntactically it looks nothing like javascript or typescript, as I will demonstrate shortly, and even from a project structure perspective, it is entirely its own beast. But before we get too deep, lets take a look those language buzzwords and break them down a bit further.

Strongly Typed

And when I say Strongly, Elm really means strongly. In Typescript, being basically a superset of javascript ES6, you can type javascript code and choose when to add some type safety. Even if you wanted to use the Type Annotations everywhere, you still have an escape hatch in the any keyword. This basically tells the type checker to overlook types for a certain variable or argument; helpful when you are trying to crank out code and dangerous in that you no longer get the type safety. Elm on the other hand leaves you no choice. You implement everything as a type, and if any of your code does not respect those types, it fails to compile. Which basically means in Elm, you do things the right way with 100% type safety, or you dont do them at all.

Purely Functional

Around a year ago, I was at a JS meetup at AWeber, a local email as a service startup, and some of their engineers were touting the benefits of Functional Programming over Objected Oriented. Basically when everything is a function, or more desireably a pure function, you get some great benefits in testing, in what guarantees you can make about your program, and in some unique data structures you can leverage. When they mean pure function they mean a few things:

  • Has no Side Effects (does not mutate any data, or do anything outside of the scope of the function)
  • Is Deterministic (for every input, exists one determined output)

So when you have pure functions, they are very easy to test (one input always results in one output), and you can compose them to make new pure functions. Your codebase then becomes a set of atomic pure base functions, and a series of functions composed of those atoms. The ideal that functional programmers strive for is to have a codebase of pure functions; however most programs need side effects (changing data in a databse, logging, sending calls to an API), and require some non deterministic components (time, random, API or database calls). The strategy is to push those components out to the “edges” of the program and keep the majority of the functions pure (A composed function is only pure if all of its components are pure).

Elm takes this concept to its logical extreme. It is impossible to write a function with side effects in Elm. It is also not possible to write a non deterministic function in Elm. Doing either will again result in your codebase faililng to compile. It is no longer a choice for a developer to write pure functions, all elm functions are pure functions. So how do you handle the side effects? It pushes all things that require side effects outside of your codebase.

Compiles to Javascript

This one is pretty straightforward. It is its entirely own language that has its own rules, but when you hit Compile, out the other side comes a javascript artifact. However unlike coffeescript or typescript, Elm has strong opinions on its code structure. Instead of MVC or MVVM patterns like Angular or React follow, Elm has its own Model->Update->View pattern. It, as a language, enforces this pattern, allowing its users to skip all of the boilerplate of their previous framework and just write the code they need.

Offers some Lucrative Features and Guarantees.

Learning Elm is a steep learning curve. It makes some previously simple things much harder, it has a syntax that takes some getting used to, and it is strongly opionated at every turn. But there are some great payoffs for taking the dive.

No Runtime Exceptions

This one honestly took me by surprise. Years of writing javascript and Angular and thousands of lines of code trying to handle, document, and record runtime exceptions told me that this is too good to be true. But Elm makes good on this promise. If your code compiles, it will not produce runtime exceptions. Period.

This is probably the best outcome of taking the functional programming paradigms to their logical extreme and enforcing them via compiler. It can pre-test all of the conditions and types and make sure that no where in the Elm produced code that there is an opportunity for an undefined or an object not to have a key.

Fast

Accordoing to their website, Elm is under 50% faster than Angular. It uses its own implementation of VirtualDOM, and using Immutable Data structures by default, it can make some crazy optimizations in diffing DOM State.

Small

For some definition of small, since there is no Framework to include, it just results in a compiled javascript artifact, it on average is much smaller than the equivalent Angular1 or React artifacts and is orders of magnitude smaller than the equivalent Angualr2 ones.

Enforced Semantic Versioning

This one came as a nice surprise to me. Elm has its own package manager, and requires any packages on it to keep the same guarantees that Elm does. Which means installing an Elm package guarantees that no side effects are happening and your codebase remains pure.

As a side benefit of the strict typing and pure functions, Elm enforces Semantic Versioning on every package. Which means when a package is released, the maintainer does not choose arbitrarily what version is released, the changes in the codebase do. So if a change breaks backwards compatibility, it is a major change, if it adds features but is backwards compatible, it is a minor change, else it is a patch. This means when you are upgrading, you can see what work needs to be done for any given release.

What Does it Look Like?

Okay so on to some code examples.

Hello World!

First off a basic Hello World.

1
2
3
4
5
6
7
8
-- HelloWorld.elm
module HelloWorld exposing (..)

import Html exposing (Html, text)

main : Html a
main =
text "Hello, World!"

Okay lets break that down. -- HelloWorld.elm - Comment. Completely optional, but shows off that comments in elm are denoted by --.

module HelloWorld exposing (..) This is the Module Definition. If we wanted to import code from this module, it would be done using import HelloWorld. The exposing (..) means we are making all of the functions defined in this module available for import elsewhere.

import Html exposing (Html, text) This is an import statement. We are importing the Html module which gives us functions for manipulating the VirtualDom. We are importing into the current namespace Html and text. If we wanted all of the Html functions we could have done import Html exposing (..), however that quickly pollutes the namespace.

main : Html a This is a type hint. It is technically optional as the compiler can infer the arity and return types, but it is best practice to declare these. This is telling the compiler that the function named main will return a Html a

main = This is declaring the function main without any arguements

text "Hello, World!" This is calling the function Html.text and passing in the argument "Hello, World!". Strings are always denoted by ". Function calls in elm require no parens, and alway use prefix notation.

More Advanced Eample

For a more advanced example, I am showing my elm-pi-calculator project available at gitlab. It was my first test project I built in elm, inspired by this video by Matt Parker showing how to calculate pi from random numbers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
module PiCalculator exposing (..)

import Arithmetic exposing (gcd)
import Html exposing (div, h1, h2, h3, ul, li, text)
import Html.Attributes exposing (property, style)
import Random exposing (Generator)
import Json.Encode exposing (string)
import Time exposing (Time, millisecond)

-- Main Function, declares the entry point for the program
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}

-- init function, gives the initial state of the model and initial command message
init : ( Model, Cmd Msg)
init =
{coprimes = [], cofactors = [], pi = 0.0, rand1 = 0, rand2 = 0 } ! []

-- Model and Type Definitions

type Rel
= CoPrime
| CoFactor

type alias Model =
{ coprimes : List ( Int, Int )
, cofactors : List ( Int, Int )
, pi : Float
, rand1 : Int
, rand2 : Int
}

type Msg
= Tick Time
| Roll1 Int
| Roll2 Int

-- Update Function

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick tick ->
model ! [Random.generate Roll1 randInt]

Roll1 randomInt ->
{ model | rand1 = randomInt } ! [Random.generate Roll2 randInt]

Roll2 randomInt ->
{ model } rand2 = randomtInt } |> updateModel |> runCalculations ! []


updateModel : Model -> Model
updateModel model =
case isCoprime model.rand1 model.rand2 of
CoPrime ->
{ model | coprimes = (model.rand1, model.rand2) :: model.coprimes }

CoFactor ->
{ model | cofactors = (model.rand1, model.rand2) :: model.cofactors }

runCalculations : Model -> Model
runCalculations model =
{model | pi = calculatePi (List.length model.coprimes) (List.length model.cofactors)}

-- subscriptions, every half second send `Tick` to `update`

subscriptions : Model -> Sub Msg
subscriptions model =
Time.every (500 * millisecond) Tick

-- View (render virtualdom)

view : Model -> Html Msg
view model =
div []
[ h1 [ property "innerHTML" (string "Π Calculator")] []
, h2 [] [ text "Pi Estimate: " ++ toString model.pi ]
, h2 [] [ text "Sample Size: " ++ toString <| getSampleSize model ]
, h2 [] [ text "Error: " ++ toString <| getError model.pi ]
, div []
[ div [ style [ ("float", "left") ] ]
[ h3 [] [ text "CoPrime Pairs:" ++ toString <| List.length model.coprimes ]
, numberList model.coprimes
]
, div [ style [ ("float", "left") ] ]
[ h3 [] [ text "CoFactor Pairs:" ++ toString <| List.length model.cofactors ]
, numberList model.cofactors
]
]
]

numberList : List (Int, Int) -> Html Msg
numberList list =
let
child =
List.map numberPair list
in
ul [] child

numberPair : (Int, Int) -> Html Msg
numberPair pair =
let
( a, b ) =
pair
in
li [] [ text <| toString a ++ "," toString b]

-- misc functions

randInt : Generator Int
randInt =
Random.int 1 100000000

calculatePi : Int -> Int -> Float
calculatePi n_coprime n_cofactor =
sqrt ( toFloat 6 / ( toFloat n_coprime / toFloat ( n_cofactor + n_coprime ) ) )

isCoprime : Int -> Int -> Rel
isCoprime a b =
if gcd a b == 1 then
CoPrime
else
CoFactor

getSampleSize : Model -> Int
getSampleSize model =
List.length model.coprimes + List.length model.cofactors

getError : Float -> Float
getError estimated =
model.pi - pi

Okay now I will unpack some of the more difficult parts.

The core of the application is the Model -> Update -> View pattern. In the beginning, in the main function, we are telling Elml what our model, update, and view functions are. We are also passing it a list of subscriptions which are another way of triggering a update cycle.

We then declare the init function, which gives the application the initial model state.

We declare a few types, Rel for relation between two numbers which can be either CoPrime or CoFactor (represening if they share any common denominators other than one). We declare our Model which has all of the numbers, slots for passing random numbers, and our calcuations for pi. Lastly we declare our Msg, which defines what kind of actions get passed around the application. In our cases, we have Tick (which is passed in via the subscription to Time, Roll1 and Roll2. We need actions for Roll1 & Roll2 because Random functions, be definition are not pure. Since we cannot write non-pure functions in Elm, elm handles random by passing the generator for random outside the elm code. We are basically passing a request for a random number out of the application, and handling the response when it comes back in.

Which brings us to our update function. update always takes Model and a Msg and returns a new ( Model, Cmd Msg) pair. Here we are using the pattern matching case function to determine which flow to follow. So when the Msg is Tick, we do nonthing to the model, and ask Random.generate to send us on Roll1 a random integer. That will exit the program get a random number, and come back in passing the Roll1 msg. update handles Roll1 by assigning model.rand1 to the integer it receives, and then requests a second random number on Roll2. When update gets Roll2 it updates model.rand2 then passes it down the pipeline via pipes (|>) to updateModel and runCalculations, which update the coprime/cofactor lists and recalculate pi.

Next we define our subscriptions, which is just passing Tick to update every 500ms.

Lastly we have our view function. View always takes the result of the update function as a model and returns Html. View builds a VirtualDOM, and is a composed function of a bunch of Html functions. Each Html function, such as div, h1 or text, takes arguements, (properites and children.) Because each of these functions is pure, and view is composed entirely of these functions, view is determinsitic as to model passed in. This allows the virtual dom to quickly rerender the view by simply diffing the model. We break down view into a few sub functions to make it cleaner, in this case numberList and numberPair. On larger projects, we would break down view into many different functions and possibly into different pages.

Other Notes and Tools

As shown above, there is a bit of learning curve to Elm. Luckily there are a few tools that make using it a lot easier.

The Nicest Compiler you ever met

While the Elm Compiler is very strict and wont let any code that may produce errors past, it is extremely helpful in telling you exactly what you did wrong and how to fix it. If there is a type-mismatch, it tells you where and why it failed and usually how to fix it. This leads to a refactoring safety that is almost unheard of. Want to change a variable name? The compiler will walk you through all the places you changed it. Library change its API out from under you (major release), the compiler will walk you through how it changed and how to react.

Elm Format

Elm as a language has some weird formatting conventions, the one that took me the longest to get used to was this one

1
2
3
4
5
list = 
[ "thing1"
, "thing2"
, "thing3"
]

At first I hated the look of this. The reason they choose this as the standard becomes really apparent when doing a codereview. Any changes to list are 1 line per change. If you add or remove an element to/from list, it is only a 1 line code change.

Luckily, you dont really have to get used to typing this weird syntax. elm-format is a package that automatically formats your elm code to be standards compliant. This means that if all of your team members install it, you have a deterministic code style that is programmatically enforced. It also helps because if you make a syntax error it wont work, so it help beginners with some instant feedback on what might be going wrong.

Elm Reactor

One of the nice pieces of the Elm ecosystem is the Elm Reactor. It is a live compiler that will recompile your elm code as you save and render it out to the browser. Any errors in compiling are nicely displayed in the browser, and whenever you make a change you can see it reflected in the browser. It is another instant-feedback-loop to make developing in Elm all that much nicer.

Elm Reactor + Debug Mode

Bonus! If you are using Elm Reactor, you can turn on --debug mode, which gives you a handy state tracker. The state tracker shows you the model at every cycle of the update function. So you click a button, which triggers an update and changes your model? you can see how that happened. Even better, you can travel back and forth in time through the state of your application to how things happened a certain way: yes Elm Gives you Time Travel Debugging. And if that werent enough, your QA team will love you because as of the latest version of Elm (0.18.0), you can export your time travel debugging log and send it to another developer to show them the state history. This means if a QA person finds a bug, they no longer have to tell you how to reproduce the bug, they can just export their Time Travel Debugging Session and send it to you for you to load and reproduce yourself.

Elm Repl

One of the biggesting things I liked about python is the ability to just open a terminal and test a few lines of code. Elm has its won repl which allows you to test out code, write one off functions and play around in.

Ellie

Ellie is an online development environment like jsfiddle that will compile and format your elm code which makes it easy to share and find examples.

Is It Ready for Production?

I have been batting this back and forth for a while. Having been an Angular1 Developer for years, and having a few HUGE Angular4 apps build, should I use Elm in Production? Here are my considerations:

Javascript Interop

Elm offers a great set of guarantees and will never through an error, however it is a very new language and is still very young. It does allow through a feature called ports, its users to interact with javascript. So you have a charting or mapping library you like? You can write a port that allows your Elm to communicate with it. Elm treats it like a backend server and basically stops any errors at the border. However going outside of Elm, you loose the guarantee that it your applicaiton as a whole wont produce errors.

Limited Ecosystem

The elm package system is still very young and there are still a lot of packages not written. This means you will be either using JS Interop or writing your own implementations for a lot of things for the time being.

Deployment Pipeline

While the Elm Reactor is nice for development, it by no means is a deployment option. Even the Elm website says to use Webpack for deploying elm, and to deploy the js artifact. This adds a bit of complexity to your build and deploy tools since you are mixing languages.

Browser Services

Modern Javascript has come a long way. Using elm by itself can be very limited, and you dont get access to things like localstorage, web workers, service workers, the fetch api, or any of the other tools build for javascript. Following the philosophy of elm out to its logical conclusion, we probably will never get most of those browser opimizations for Elm natively. This means that you will eventually need to write some level of Interop.

Works With Webcomponents

One light of hope that has shown up is the ability to work with Web Components inside of elm without the need for interop. This potentially simplifies and cleans up a lot of the handling, but still leaves opportunity for error.

Styling is a Pain

Out of the box, styling is a pain. There are projects, like elm-css that look to make that easier. There are also projects such as elm-mdl that wrap CSS libraries in elm pure functions that make it super easy to build nice applications. This is definately an area that will see some improvement as Elm matures.

Conclusion

Elm is a really interesting language. The architecture it enforces really forces developers to think ahead as to how their application works, what sort of data is flowing through their application and what to do in certain edge cases. The feature set Elm offers is amazing and perfectly suited for small one off projects.

For larger projects, I would suggest going with React/Redux for the overall project, and slowly working in certain components with elm replacements. This way you still have all the power of javascript readily available to you, but have components where you get all of the safety of Elm.