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.
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.
And when I say
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.
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.
This one is pretty straightforward. It is its entirely own language that has its own rules, but when you hit
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.
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.
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.
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.
Okay so on to some code examples.
First off a basic
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
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
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.
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.
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
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
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
Roll2. We need actions for
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
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 by assigning
model.rand1 to the integer it receives, and then requests a second random number on
Roll2 it updates
model.rand2 then passes it down the pipeline via pipes (
runCalculations, which update the coprime/cofactor lists and recalculate pi.
Next we define our subscriptions, which is just passing
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
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
numberPair. On larger projects, we would break down
view into many different functions and possibly into different pages.
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.
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 as a language has some weird formatting conventions, the one that took me the longest to get used to was this one
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.
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.
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.
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.
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:
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
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.
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.
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.
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.
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.