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:
- Elm restricts the way you program, resulting in maintainable code no matter what.
- There are no runtime exceptions so debugging is way less of an issue.
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.
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:
- Strong static types → finding errors fast with readable compiler messages
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:Embedded content: https://twitter.com/ohanhi/status/652368381182672898
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.
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 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)
!(https://images.contentful.com/pqts2v0qq7kz/5EpZrBspRSOsI4YKg2gQQm/5f5069467c8d89fb4dd0c7f808d86306/our-architecture_s1800x0_q80_noupscale.png)* 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.
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!
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.
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.
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!
- Ossi HanhinenHypertext Elementalist