Lessons learned with Elm

I had been working with Elm for more than two years. I was new to the language and statically typed functional programming too. I mainly come from a dynamically typed programming background (JavaScript, Python) but I had functional programming experience through Emacs Lisp. Hereby I’m collecting my insights and lessons learned. Since Elm is not only a language, but also a runtime and a framework, I will cover these aspects respectively.

Language and compiler

I have to admit that it took some time until I got used to the syntax of the language. (I had no prior experience with ML languages.) Eventually I find Elm’s syntax very clear and explicit, and now I love it. Especially after working with TypeScript. With help of elm-format, there’s even one preferable way to write Elm code.

One of the first surprises was that partial application is not only a feature you <i>may</i> use. Partial application is part of the core language. All Elm functions take only argument and return a function with the rest of the arguments. Therefore syntax-wise, there’s no difference between defining a constant value or a function that takes no parameters. It makes things so simple!

It’s liberating how safe and fun is to do refactoring an Elm code base. Static typing combined with Maybe, pattern matching, and Elm’s friendly compiler messages are incredibly helpful. Change the name of a binding or a function’s type signature, and the compiler will tell you exactly what you need to correct. (Almost on a par with Haskell’s type-hole feature.) Introduce a new type value for a custom type and the compiler will let you know of unhandled cases. No more guard conditions to make sure you access the value you assumed. No more unit tests to make sure you handle invalid input cases. When refactoring becomes this easy, improving code design becomes habitual.

I remember I initially struggled a bit with the Maybe type. It was annoying that I need to handle all the possible cases of absent data (sic!). Now I find it a powerful tool for explicitly stating if a certain value can be missing, and the compiler will force you to handle that case. No more Cannot read property 'context' of undefined errors in production and such. (Interestingly, when it comes to certain data types, such as List, that have already an empty value, the type Maybe List offers two blank states: Nothing and []. In this case, it’s recommended to use custom union types to be more expressive.)

Type aliases provide an elegant way to reuse existing types and give them a more meaningful name in the given context. However, when it comes to record IDs, they are often best described as custom types. This way the compiler can make sure that only the right type of value can be passed around. Furthermore, opaque types also advocate an API that doesn’t expose implementation details.

Architecture and runtime

Elm is not only a language, but also a very opinionated architecture that was designed to build webapps with. Think of it as TypeScript, React and Redux bundled together. In fact TEA, The Elm Architecture, inspired frameworks such as Redux. TEA is simple, straight-forward with no hidden magic. Regardless of the complexity of the web app, it all boils down to a Model data record, an init, update and view function. Therefore there’s only one way to build a web app with Elm.

A simple view of The Elm Architecture
The Elm Architecture (TEA) animation — source: Lucamag

Elm is a purely functional language. Elm user-space functions have no side-effects. Interactions with the outer world (such as sending a HTTP request) are wrapped in the opaque type Cmd that is then passed to the Elm runtime. When there’s a response available, the runtime will call the program’s update function with the appropriate message. This model is extremely powerful and guarantees type safety in the entire program.

While there is a strict convention to follow for an Elm app’s architecture, there’s plenty of room space to decide how to tackle internal application complexity. A conclusion that I arrived a bit late was to keep the sub component’s signature as close to Browser.element, Browser.application as possible and use the effect pattern to abstract away Cmds, and therefore make the application testable by elm-program-test.

The articles If you’re using React, Redux and TypeScript, you would be so happy with Elm! and React Redux Thunk vs Elm are two helpful comparisons of Elm and an equivalent JavaScript stack.

Code organization

Everyone carries code organization and refactoring best practices they picked up in the past. However, they aren’t necessarily meaningful in Elm. The official guide suggests to focus on finding the right types and data structure first to describe the problem, and worry about file size later. Organize your code around types, not components or the MVC pattern. As I mentioned before, in the end of the day what you need to provide is a single Model record and the update and view functions.

In his talk The life of a file, Evan Czaplicki shows his approach of figuring out when to split code. He illustrates the problem with two checklists that would give the idea for many of abstracting away a checkbox list component. While implementing the business logic, it turns out that eventually there’s very little in common between the two solutions and early abstraction would have lead to poorer code design.

Alex Korban, author of the book Practical Elm, also shares his take on code organization. He points out that “you are still ultimately passing a single update function and a single view function to Html.program” when working with the Elm architecture.

Finally, Richard Feldman, author of elm-spa-example, demonstrates different refactoring techniques that come handy as your code base grows. He discusses narrowing responsibilities on various levels.

Learning Elm changed how I work as a programmer. I model the problem with types first, and then basically the implementation just follows along.
Make impossible states impossible is my every day mantra even when working with other languages. But here’s the thing: Elm really helps you to map out all the state transitions. Pattern matching is the main tool to achieve this. Elm too supports a catchall pattern like in other languages however I learned to avoid it whenever possible. Without a fallback case, whenever I introduce new type values, the Elm compiler will tell me to handle the missing branches. This gives me an opportunity to revisit all the places where I might need to add a new business logic, instead of just silently doing the fallback action. This approach, however, might result in lots of code. That’s another thing I learned no to be afraid of. It’s lots of code but because it’s incredibly explicit. One of the most common complaints about an Elm code base is that it’s too verbose. I believe the right way to handle this in the Elm world is via code generation. I didn’t have a chance to work with it.

Finally, thanks to the Elm language server, should you use Emacs or VSCode, you get IntelliSense and all the bells and whistles you might need. However the biggest aid is still the language and the Elm compiler itself. Thanks to that, it’s possible to have tools such as the elm-review that can remove dead code automatically and safely.

Learning resources

There are many great resources available to study Elm.

The Zen of Elm

Inspired by The Zen of Python, I compiled a verse of patterns and best practices in Elm.