Written by Robin Heggelund Hansen and Kjetil Valle

Vy is the largest transport provider in Norway. Their website has over a million visitors each month, and people depend on it every day to purchase bus and train tickets. Another interesting fact about this website is that it’s almost entirely written in Elm.

13 Elm applications make up the non-static parts of the website. These applications rely on 7 private packages for common functionality, and only a little bit of JavaScript for certain operations not directly supported by Elm. All in all, this amounts to about 83,000 lines of code, a number which continues to grow by the day.

Photo: Vy/Mads Kristiansen

Early experiments

We have had Elm in production at Vy since late 2016. We started out by testing the language out on a small internal application to get a feel for its capabilities and limitations. We found that the strong type system prevented several classes of bugs in our application, and the alluring promise of no runtime exceptions was confirmed with a big boost to our productivity. In short: we really liked it! Yet, we were still were unsure whether it would scale to “real use”, both in terms of making something more complex for end users, but also in terms of introducing a new language to the team — and a functional one at that.

To test out the latter, in the summer of 2017 we tasked a team of summer interns with the renewal of our seat map application, a crucial component of the ticket booking process. They were to use Elm, a language they had no prior experience with. To our surprise they took to the language very easily, and their work turned out great. The application they wrote is still running in production with only minor changes since.

This experiment gave us the confidence we needed to try out Elm on a larger scale. As luck would have it, a decision had been made to re-write our entire booking process. Vy’s vision is to make it easier and more accessible to purchase tickets, so that even more people can travel using environmentally friendly public transportation.

Fast forward to today, and the entire booking process has been rewritten in Elm, as well as most other parts of the website.

New developers quickly became productive

We have found it surprisingly easy to get new people up to speed with our projects. Starting out as only a few die-hard Elm fans, we have quickly grown to 15+ Elm developers spread across multiple teams. Several rounds of summer interns have also proven that it is possible to learn Elm and our systems, and become productive in a matter of days. We have achieved this through a combination of “Intro to Elm” workshops and liberal use of pair programming.

An approach we have found especially useful is to find out what the person is most comfortable with, and start with those parts of the code base. For instance, for someone with deep knowledge about HTML and CSS, we will probably start by having them work on the view code. For someone who primarily knows backend coding, perhaps working with the API integrations is familiar ground, and a good starting point. Once they feel comfortable, it’s easy to expand and have them look at new aspects of the application.

There is boilerplate, but that’s fine

We believe that a big reason behind our success in getting new developers rapidly up to speed is due to some of the core design choices behind the language. Namely that Elm is a small language with features that favor explicitness. The type system is involved in everything you do, and there are few tools to abstract away repetitive code outside of wrapping things in functions.

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. It is not possible to do as you would in JavaScript and pretend that everything is OK until it isn’t, and then catch those cases in some exception handler at the top of your program. 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 has lead to Elm being criticized for not having the tools to avoid boilerplate. In our experience, however, this is a good thing. 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. Elm apps also tend to be structured in the same way. If we want to see what sort of things can happen, we simply look at the top level update function. If the app is structured a little differently, it’s easy to figure out how things are meant to work by simply starting at the top level Main.elm file.

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.

It does have rough edges

We’ve also had our share of frustrations. As mentioned earlier, some of our code resides in packages which are re-used in most of our frontend facing apps. Elm, however, doesn’t support private packages. We ended up using a third party tool for this, called elm-git-install, but it doesn’t solve all our issues. It works fine when we have an Elm application relying on a private package, but private packages are not able to rely on other private packages. This has forced us to include a frontend app within each package repo for the sole purpose of building the package with its dependencies, so that we at least know it should work outside the repo.

While this is not a show stopper, it is a little awkward and there are several developers who view this as a sign of language immaturity.

Another thing that can be an issue is the small number of third-party packages. This is partly due to Elm’s young age, but also because of a core design decision in the language.

The only way Elm can make its promise about no runtime exceptions, is by being strict on what kinds of packages are available. This means that JavaScript cannot be included in packages deployed to Elm’s package registry, only libraries with pure Elm code are allowed. In many cases, JavaScript interop becomes unavoidable.

Fortunately for us, this hasn’t been that big of a problem. We only have a total of 1,100 lines of JavaScript code across all of our applications, most of which are duplicated for each app. However, there was a point in time when this was not the case.

Last year we wanted to have phone number validation in one of our apps. There was no Elm package for this at the time, so we imported a JavaScript library and called it through Elm’s mechanism for JavaScript interop: ports.

Ports work asynchronously, which is fine for certain things like local storage or websockets. But for validating phone numbers, which ideally is a synchronous operation, this introduced a lot of unnecessary complexity. We now had to consider when phone number validation occurred, and what could happen if the user was to click the submit button just after entering their phone number. This did end up causing a few bugs which were surprisingly difficult to find, given the size of the feature.

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.

Elm is here to stay

The thing that got us interested in Elm in the first place was a wish for a good static type system, along with the guarantee of no runtime exceptions. After two years of experience with Elm in production, we’ve found that these promises result in a definite boost in our productivity. Team members, both experienced and inexperienced alike, feel more confident when making larger changes to the code base than we do in other languages.

Because of this, it has been easy to scale from a few Elm enthusiasts in one small application, to having a significant portion of our website in Elm, supported by a growing team of Elm developers. When refactoring is easy, a lot of the pain associated with scaling fades away.

A common misconception is that it is risky to use a non-mainstream language, since it will then be difficult to find developers with the right experience. We have found, however, that we don’t need people to know Elm beforehand. We have onboarded people with different technical backgrounds, and they tend to pick up the language rapidly and become productive within days.

We have found Elm to be a joy to work with, for the most part. There have, of course, been some issues here and there, but nothing we haven’t been able to work around. In extreme cases, if we cannot do something in pure Elm, we can always use an escape hatch — or rather, an escape port — and do it in JavaScript. But the instances we’ve needed to do this have been few and far between.

In total, our experience working with Elm has been overwhelmingly positive, and we look forward to continue working with it going forward.

Thanks to Aksel, Fredrik, Sonja and many others for proof reading and feedback.