loading...

One year of Elm in production

tzemanovic profile image Tomáš Zemanovič Originally published at tzemanovic.github.io on ・4 min read

About a year ago, I started a new job where I build tools aimed at helping others provide a better user experience. As is common in the software industry, I inherited lots of poor vanilla JavaScript code. There were no tests, very little code structure or code re-use, mixed coding styles and as expected, a full menagerie of bugs, which frequently cropped up resulting in very poor user experience. With such a sorry state of things, it only made sense to try to take a wholly different approach - stop writing JavaScript and instead pick a higher level language that would make sure all the things are kept in check.

As a functional programming enthusiast, I’ve been a keen user of Elm for some time and although I conceptually liked it better before version 0.17 with its higher-level abstraction of rendering visual elements contained in elm-graphics library, the inclusion of HTML library made it possible to integrate it into existing front-end ecosystem with much less friction to the point where it actually became one of the top choices.

If Elm code looks terrifyingly unfamiliar to you, fear not. It builds on top of very mature ML (Meta Language) family of programming languages. Elm itself is written in Haskell, which is as old as programming itself, but you can find a plethora of languages in this family:

PureScript front-end language, more similar to Haskell
Idris general purpose language similar to Haskell, but with dependent types
Scala fusion of FP with OOP for the JVM
Frege Haskell for the JVM
Eta Haskell for the JVM
OCaml FP with OOP
F# Microsoft’s FP with OOP
Hobbes custom language built for embedding in C++
Standard ML general purpose popular language for compilers

As processors scaling predicted by Moore’s law is close to reaching its hard physical limits (as Herb Sutter famously put it the free lunch is over1), the software industry will have to reach to tools that are better suited for dealing with concurrency. And these tools are functional programming languages. The interesting thing is that even if your application has no need to deal with concurrency, it can still benefit from FP, its foundation in mathematical concepts scares a lot people off, but it is its strong point. If the word mathematical puts you off, know that you don’t actually have to know about this to take advantage of it, just like you won’t fly off a rollercoaster if you don’t know its physics (anecdotally, it will probably be your last ride if the rollercoaster has been built with complete ignorance of physics).

As most of software development education and training focuses on OOP, there are lots of developers who get the impression that thinking about things in terms of objects is somehow natural. But the limitation of this mindset becomes very apparent in a very popular OOP topic - Gang of Four’s design patterns2. It also becomes apparent when implementation becomes so complex that no one can or even wants to work with it anymore. But where does this complexity come from, is it intrinsic to the problem domain? Most programmers know that tight coupling between unrelated concepts creates problems, in the same spirit I would say that the interleaving of data with behavior inside classes can create more problems than it solves. The declarative nature of FP languages removes much of the incidental complexity, the elephant we’re dragging around3, introduced by imperative programs because we don’t need to think about execution to understand what is going on. In more conceptual words of professor Philip Wadler, some languages were discovered while others were invented4 and you can tell.

The nice thing about Elm is that the community that has formed around it is very inclusive and welcoming. The language itself is very beginner friendly, to the point where some mistake its simplicity for a toy language. Don’t make the same mistake though, because Elm is up there with big names such as React, in fact, it is much more a complete solution than React. The simplicity of Elm is rooted in hard work that has been put into its design.

Once you familiarize yourself with it, the Elm compiler will be your best friend. Its error messages are so clear and helpful that they’ve influenced other languages, such as Rust. The language comes with a package manager elm-package and the libraries released in Elm automatically adhere to semantic versioning. You can even use it to check what has changed between different versions of a given package. For interactive coding, there is elm-repl and elm-reactor. The performance of Elm generated code is great and when needed optimization is a simple function call away.

So far, Elm’s simple yet powerful design has helped us to stay focused on our goal of building a great product and we expect that the payoff will be even greater in the long run. At the moment, we have about 16k Elm lines of code in production, steadily translating pleasant development experience into pleasant user experience and we have more on their way.


  1. Herb Sutter: The Free Lunch Is Over

  2. Ted Newark: Why Functional Programming Matters @ Devoxx Poland 2016

  3. Rich Hickey: Simplicity Matters keynote @ Rails Conf 2012

  4. Philip Wadler: Propositions as Types @ Strange Loop 2015

Discussion

pic
Editor guide
Collapse
kspeakman profile image
Kasey Speakman

We've also had Elm in production for about a year now. I really can't say enough good things about it. Using Elm is probably the first time after I deployed a UI app that I didn't feel dread about maintaining it. And my team feels the same way.

We did make some mistakes that hurt maintainability initially. But Elm is really safe to refactor.

Collapse
ok32 profile image
ok32

Hey. I'm curious to know what were the mistakes.

Collapse
kspeakman profile image
Kasey Speakman

Probably the biggest mistake was in how we designed the Model. We tried to use it as a "database". So for example, when we switched to a page, we would load the data for that page into a property on the model. Then we would reuse the data on another page. However, this led to a lot of code just to check/maintain this shared data. And it was the source of many state-based bugs. (One page left the data in X state, but another page expected it in Y state. Or one page needed field Q but the other page didn't for its view.)

Ultimately, our use cases required the app not be offline-capable and our users really preferred getting the latest data from the API any time they switched pages. So we dropped this shared state model. We went to a strategy where the model represents the state of UI elements. So, pages are part of a DU (aka custom type) because only one can be shown at a time. So when the user switches pages, all the data from the other page is gone. No need to reset it or make sure it is left in a certain state.

So in the end, our model ends up looking more like this. Code is off the top of my head.

type alias Model =
  { apiToken : String
  , currentPage : Page
  }


type Page
  = Home
  | OrderView OrderViewState
  -- others like OrderList

type alias OrderViewState
  { orderId : String
  , orderData : Remote OrderData
  }

type Remote a
  = Loading
  | LoadFailed Http.Error
  | Loaded a

The way we initially did state also led to weird extra routing types. Those became unnecessary once we switched to this model. It maps very cleanly to/from navigation URLs. It is easy to parse the URL /order/{orderId} and convert it into the OrderView page:

let state = OrderViewState orderId Loading
    page = OrderView state
in { model | currentPage = page }
   ! [ Api.getOrderData model.apiToken orderId ]

Going the other way is easy too.

case model.currentPage of
  OrderView { orderId } ->
    "/order/" ++ orderId

Anyway, I think modeling as a database was the main thing that we did wrong at first.

Thread Thread
ok32 profile image
ok32

Got it. Thank you!

Collapse
eljayadobe profile image
Eljay-Adobe

I have a tough time talking about FP to OO developers. I often get the deer in the headlights look.

Must be my fault, as the messenger.

I have not been able to convey to OO developers that things like Design Patterns and SOLID are important OO disciplines to shore up the areas where OO has weaknesses.

And that the strengths of FP -- such as immutability, recursion, pattern matching, higher-order functions, code-as-data, separation of behavior from data, referential transparency -- are, collectively, game-changers from the OO paradigm.

Collapse
tzemanovic profile image
Tomáš Zemanovič Author

I can definitely relate to that. I can think of some things that are probably at play:

  • what is unknown can appear as scary
  • the common myth that FP is somehow harder than OO; I would say this is more to do with education, which is better in OO than FP. OO also receives much more focus.
  • FP is sometimes perceived as elitist
  • for some areas FP might not be the best pick, if your main focus is to get something done as quickly as possible; For example, there is a very handy State of the Haskell ecosystem post, in which Gabriel Gonzalez rates Haskell for each programming area. For many applications the trade-off can be diminished if you're planning to support them for some time, which we usually do anyway.