The Elm Language
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 | -- HelloWorld.elm |
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 | module PiCalculator exposing (..) |
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 | list = |
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.