Vy.no, the largest transport provider in Norway, rewrote its website in the compile-to-JavaScript Elm language. Robin Heggelund Hansen and Kjetil Valle presented in a recent article the three-year journey by which Vy gradually switched to Elm for the non-static parts of its website.
Vy.no’s website allows a million of visitors every month to purchase bus and train tickets. From early experiments in 2016 that included Elm in part of Vy’s website, the non-static parts of the website today are almost entirely written in Elm, with sprinkles of JavaScript to support operations not handled within Elm. Thirtheen Elm applications make up the website, representing a total of 83,000 lines of code.
Valle reported that Vy’s Elm journey started with using the language in a small internal application, which allowed him to check the effects of a strong type system on common types of bugs. One objective of the trial was to gain a first-hand experience on the no runtime exceptions claim of Elm.
After a positive experience, less than a year later, the next stage was to grant Elm the responsibility of handling a larger part of the website. To that purpose, a team of summer interns with no prior Elm experience were assigned to write a component of the ticket booking process. The team of interns reportedly picked up the language quickly and produced code that is still running in production today, with only minor changes occuring in the interval.
When a decision was made to rewrite the entire booking process, the development team decided to give Elm responsibility for greater parts of the website, starting a process which ended with the entire booking process being rewritten in Elm, as well as most other parts of the website. The development team today consists of over 15 Elm developers in multiple teams.
While the overall experience with Elm was good, Valle mentions a few important caveats. Elm code can be repetitive and include boilerplate. Valle explains:
Let’s look at error handling as an example. In Elm, you have to wrap everything that can fail into its own type. If you want to extract the value of that type, you first have to check if something did in fact go wrong. There’s no avoiding that. (…) You can’t write one error handler that works for all kinds of errors either, as Elm has no way of declaring that two types adhere to the same contract.
This relates to a common complaint among developers who have tried and later discarded Elm as a language, which is the absence of type classes. As a matter of fact, the support for type classes in Elm is one of the first issues created (in 2012!). While the language has three built-in type classes, it does not support user-defined type classes. This means a developer wanting to map a data structure must use the dedicated map function adapted to that data structure, e.g. List.map
, Array.map
, or Dict.map
, instead of just simply using a polymorphic map
. This is poised to create boilerplate, and limit the expressivity of the language and the ability of developers to define complex type abstractions. Elm additionally removed in its latest version (0.19) user-defined infix operators, adding to the amount of code which had to be explicitly written by developers.
Valle nonetheless argues that in his experience, the absence of type classes and other conveniences is a good thing. He says:
Because of how explicit Elm apps tend to be, it’s easy to understand what the code is doing without being very familiar with it. To figure out if a function can perform any kind of side effect, we can simply glance at its type signature.
While boilerplate tends to be viewed as negative in many languages and frameworks, we have found it to be a simple and important guide to how a frontend application is set up, and by reading it we’re able to better understand how a given system works.
Another pain point mentioned by Valle is that JavaScript interop can be complex, given Elm's self-imposed limitations. Elm does not support private packages, meaning that JavaScript cannot be included in packages deployed to Elm’s package registry. This makes interop necessary, with the added complexity that it is necessarily async (due to asynchronous message passing). Valle mentioned this ended up causing some difficult-to-find bugs when handling phone number validation on the JavaScript side.
On the bright side, keeping JavaScript code at arm’s length is a strong reason behind Elm’s ability to claim the absence of runtime errors. This also incentivizes developers to develop in Elm most of the functionalities that they need, resorting to interop only when absolutely necessary. Valle noted:
Fortunately for us, the “elm-phone-numbers” library came along and we were able to replace all our asynchronous validation code with three simple lines of Elm code. But the lesson we learned is still relevant: JavaScript interop can be quite painful, especially when converting a synchronous API to an asynchronous one.
Valle concludes by noting that the benefits Vy experienced by using Elm are the absence of runtime exceptions, a boost in productivity, quick onboarding of developers, and easy refactoring. In his opinion, this largely trumps the pain points encountered when using Elm (like JavaScript interoperability, or boilerplate).
Elm’s latest version 0.19.1 was released on the 21st of October 2019, more than a year after Elm 0.19’s release. There is no known timeline for Elm 1.0. Elm is a domain-specific, compile-to-JavaScript functional programming language for creating web applications. The Elm design process emphasizes usability, performance, and robustness.