fb-pixel
Back to Blog

How Elm made our work better

This article was originally titled "Elm in the real world", but it started to sound strange to me after Evan mentioned about it. - Ossi, Jan 16th 2016

Elm is a beginner friendly functional reactive programming language for building web frontend. Choosing Elm for a customer project made my job nicer than ever and helped maintain project velocity during months of development. This boils down to two things, in my opinion:

  1. Elm restricts the way you program, resulting in maintainable code no matter what.
  2. There are no runtime exceptions so debugging is way less of an issue.

At the Reactive 2015 conference, where I gave a lightning talk on stateless web UI rendering, many people asked me: "How hard is it to debug compiled Elm code on the browser?" I was more than a little confused by these questions, until I remembered what it's like to write JavaScript. You make a change, switch to browser, set up debugging stops, click on a few things in the app, check the debugger and go "Uhhh... How did that happen? Maybe I should console.log something?"

Writing Elm, on the other hand, is like this: you make a change, check the superb compiler errors, fix them. Next. Of course, you should then switch to the browser and check that it actually does what you wanted, but the point is: you don't spend half the time coding digging through the debugger.

Elm compiler errors can be super helpful

Despite being a whole new language to learn, I would argue the gains of the language design can far outweigh the "loss of time" while learning. JavaScript is, after all, a very very convoluted language with myriads of different libraries, frameworks, linters et cetera to try and make dealing with it less painful.

Based on my experiences, I wholeheartedly recommend Elm for all bigger frontend application projects from now on. There are parts that are more cumbersome than in JavaScript, for sure. But all things considered, I value the guarantees Elm provides far more than the ability to do things quick 'n' dirty.

How we chose Elm

In the Summer of 2015 I had a stroke of luck. I got into a project, where there were no restrictions on the frontend technology. We were tasked with building a web application for a select few expert users, from scratch. The browser support target reflected the fact: Latest Chrome.

I sat down with Henrik Saksela to discuss our options. The baseline assumption was that we would use React to build the frontend, but I wasn't convinced of its merits. We talked about Cycle.js, ClojureScript and Reagent, and my recent endeavors with Elm.

In the end, we decided to just give Elm a try and quickly fall back to Reagent or React if it doesn't work out. We figured we should try and make the trickiest parts (technology-wise) first, so we could fail early.

Here's how I originally put it in the project README:

Less complexity

The main properties and benefits of using Elm instead of plain JavaScript are the following:

  • Strong static types → finding errors fast with readable compiler messages
  • No null or undefined → impossible to leave possible problems unhandled
  • Immutability & purity → readability and maintainability
  • No runtime exceptions → uncomparable reliability
  • Reactive by design → FRP isn't opt-in, it is baked right in the language

Months later:

Heart illustration

We’d like to help you make a fully informed decision about cookies. Accessing some of our embedded content that could be of interest and use to you requires you to enable cookies.The choice is always yours.

The project

The application was a tool for quickly managing news website content. In essence, the articles on the site's main pages are curated by a handful of experts 24/7, and our tool was the means for doing that efficiently. Futurice was also responsible for the design, both user interaction and graphics, and building the backend service, so we had a great cohesion within the whole project.

The interaction was heavily based on drag-and-drop. To place an article on the page, the user would drag it from the side panel into the main panel. Similarly, modules (article groups) could be dragged up and down on the page to determine their order.

Architecture

Note: Back when we started the project, StartApp wasn't as big a thing as it is now. It might have guided our approach to a different direction, but we feel our architectural choices resulted in a great solution in this case.

The Elm Architecture outlines the basic pattern of Model, Update and View. This is in fact a mandatory separation in any Elm application. The language just works that way and there is no way around it.

Everything in Elm is immutable, from "variables" to function definitions to records (which are a bit like JS objects). That means rendering a view, for example, cannot possibly have an effect on the application state. And that the dreaded "global state" is actually a very nice thing - since we can be sure nothing is changing it in secret.

The Elm pattern

The Elm pattern is the following:

  • Define the shape of the data (model)
  • Define Actions and how to react to them (update)
  • Define how to show the state (view)

Dissecting the state

Having worked with Virtual DOM and immutable structures before, me and Henrik reasoned we could try and rely on the backend data -- forgoing frontend state completely. This simple idea worked out really well for us.

We came to think about "application state" in this manner:

  • The UI can be in different states regarding views, ongoing drag-and-drop actions and so on, which should not persist between sessions. This is our UiState.
  • The backend represents the real state of the world, or all data that should persist. This is our DataState.

UiState was handled like in any other Elm application, updating the state based on Actions.

The way we handled DataState was a bit more involved than the standard pattern, though:

  • Define the shape of the data on the backend (model)
  • Define Actions that get turned into Tasks
  • Define HTTP call tasks that get turned into succeed/fail Actions
  • Define how to react to the succeed/fail actions (update)
  • Define how to show the state (view)

* Our model for pessimistic UI update*s

How our pattern differed from a standard Elm application was that instead of updating models immediately based on an Action, we used the actions to determine which HTTP calls are necessary to comply to the user's intent. These calls then resolve to either a failing or succeeding scenario. Both of these are then translated to updates - be it showing a notification about the error or changing the data. In short, we had a fully "pessimistic" UI that would save the state to the backend on every change. Pessimistic means that we never assume an operation to succeed, but instead we rely only on facts: what (if at all) the server responds.

The way we update the backend-provided data in the Elm application was the main kicker, though. Once we've POSTed a change to a list in the backend, we simply GET the whole list from the backend and replace the whole thing in our model. This means the state can never be inconsistent between the backend and the frontend. We also made sure only one of these tasks could be running at once simply by deferring data-changing user interactions until the UI had updated. The user could still scroll and click on things while the task was running, but not drag things from one place to another (which would imply a data change).

There were two main concerns to this approach: 1) is the UI responsive enough with backend-only updates, and 2) is it madness to discard and replace the whole model on update. As it turns out, concern 2 was mostly unfounded. The Virtual DOM in elm-html does the heavy lifting, so on the browser only an updated item gets re-rendered. Concern 1 was valid though. As previously stated, our project was an expert tool. It would only be used from within the customer network, using desktop computers. In our experiments using a wireless connection (actual users have wired connections), we found the heaviest updates took about 600ms on average. This was before we optimized the caching, which sped things up ten-fold. As a result, pretty much all updates happen in consistently under 300ms, which is great!

Strictness - a mixed blessing?

The strictness of Elm proved invaluable. Since the compiler won't let you disregard a potential failure even when trying out something, there is no way some of them might end up in production code. Coupled with total immutability the language itself enforces good functional programming practices.

The place where Elm's restrictions can become hardships are when dealing with the outside world. For one, you need to provide full modeling of the data structure if you wish to parse a JSON response from the server. And you need to take into account that the parsing may fail at that point. This all seems obvious once you get familiar with Elm's type system, though. If your API is a little "tricky" - for example the response JSON can have certain properties that define which other properties are available - you may need to jump through hoops to make that work.

Another thing is interoperability with JavaScript libraries. First off though: Elm has its FRP and functional utilities built-in, and the elm-html package comes with virtual-dom, so there's no need for stuff like Lodash, React or Redux.

But if you do need to interact with JavaScript, the mechanism for that is called Ports. Ports are essentially strictly typed event streams to listen to (Elm to JS) or to push events into (JS to Elm). This means you will need to write some boilerplate-ish code, both in Elm and in JavaScript, in order to pass a message to the other side. So the more JavaScript libraries you need, and the more different kinds of objects you want to pass through, the more boilerplate code you will end up with.

That said, Elm is still fairly young and seems to be quickly gaining recognition in the aftermath of the "functional web frontend tsunami" React and friends brought about. The fact some commonly used library alternatives are still missing could soon turn. And while we're on the topic of dependencies, Elm has one more ace up its sleeve: the package system enforces semantic versioning. Because of the strict typing, the package manager can infer any outfacing changes to the package source code and determine the version number on its own. No more sudden breaking because an NPM package upgraded from 0.14.3 to 0.15.0!

Conclusions

Go and learn Elm. Seriously. It is the simplest language I have ever tried, and the team has put a crazy lot of effort into making the developer experience as nice as possible.

The syntax may seem daunting at first, but don't fret. It's like getting a nice new sweater. A few days in, you'll be familiar with it and from then on it's like you've always known it. In our project, two out of three developers had never coded in Elm before. Both of them got up to speed and were productive in a couple of weeks. Even the sceptic told me that once he got over the initial shock, he found Elm a very nice language and a good fit for the project.

A compiled Elm application has a whole FRP implementation built in, so it might not make sense to use it for very small things on a mostly static web page. For these kinds of uses, you may be better off with e.g. PureScript. PureScript is, however, much harder to learn without a solid prior understanding of functional programming concepts. There are other differences between the two as well, such as the way they handle data. Elm uses persistent data structures under the hood, which means better performance in big apps. PureScript resorts to standard JavaScript data structures, which results in smaller compiled files.

To get started with Elm, I recommend reading through the official Elm documentation and checking out the links at Awesome Elm. When I was first learning the language, I wrote an introductory article that describes the basic syntax and the model-update-view pattern: Learning FP the hard way.

Have fun!

If you liked this article, please consider sharing it with others who might as well!


Thank you for the proofreading comments and clarification suggestions, Harri Hälikkä, Andre Medeiros, Henrik Saksela and Richard Feldman. You were most helpful!

Author

  • Portrait of Ossi Hanhinen
    Ossi Hanhinen
    Hypertext Elementalist