I’ve been using Elm for the past two years. During that time I’ve written about 34k production lines of code in Elm across four projects. That work spans code from my very first day until today. My overall progression looked something like this.
A major part of Elm’s pitch is that it has no runtime exceptions. Does it mean there are no bugs? No, but they are much less frequent than in more orthodox languages. For me, most bugs actually appear in the interface between Elm and its impure environment.
How do you get into Elm and how to use it on a daily basis? Is the hype around Elm justified? Are the detractors right? Read on to find my take on the issues.
Disclaimer: Elm is my overall favourite language.
How I learned Elm
Effectively I’ve learned enough Elm to write my first production code within a couple hours. Then it took me a couple more weeks to get a fuller grasp of what’s going on. These here were roughly my stages of progression.
Official Guide (, “Elm in Action”)
In the beginning I’ve started with the official guide. It’s a great place to start as it provides an overview of the language. However, for a beginner it can lack a little context. Often programming books provide more context, such as toy applications. Check out the new book by Richard Feldman (@rtfeldman) “Elm in Action” for a more complete guide.
Coding with Django and Phoenix
While and after reading the guide, I’ve experimented with elm-reactor (seems to be abandoned/reworked now). Then I’ve integrated Elm with an ongoing Django project. Finally, I started to write some production code on the first day. The code still runs today, unchanged.
That initial production code was for an admin login. It was not necessary to rewrite that login interface, but it was a good opportunity for practice. Although today that’s an easy task for me in Elm, it took a little tinkering before I got it right on the first day. Don’t get frustrated, stay with it, after a little stubbornness usually things become clear and your brain wraps around new concepts, which then become second nature.
Another good source for me was reviewing Richard Feldman’s RealWorld example app. Richard has a ton of experience writing in Elm, so his code is an excellent source to learn from.
Join the community
Additionally, the usual applies: search for problems on Google/DuckDuckGo, read StackOverflow, check out the Elm forum. Consider joining the Slack channel. Code a lot, grow your code base, think deeply about the problems you code and how to solve them idiomatically, research and read. Grow larger projects and grow more complex projects.
Recently, I’ve discovered exercism for myself. Doing a track on exercism kind of feels like cheating when progressing through a new programming language. I’m currently using it to learn Haskell, but I’m sure that the Elm track is great as well.
How I’ve been using Elm
I’ve been using Elm in a legacy project with Django (👍) and in new projects with Phoenix (👍👍). Both are relatively similar with regards to the integration with Elm, but both also have their quirks.
In both frameworks I began with server side rendering. Initially, I’ve started to replace existing features and then I began to add new features with Elm instead of templating. Most of the time I’ve used Elm for features that benefited considerably from interactive dynamism. But often enough I’ve also implemented other features in Elm, just for the heck of it. Try it out, it’s fun and provides good practice.
In both frameworks I’ve used webpack to get Elm running. It’s a pain. Each time I ended up wasting hours to get Elm running properly with webpack. This is not Elm’s fault, but still this nags me.
Once webpack is set up, it’s great though. During development webpack will trigger the compiler on file changes. In Phoenix you even get an automated page refresh out of the box.
In both frameworks, I keep the Elm code in the corresponding global static dir: `assets/Elm` for Phoenix and `static/Elm` for Django. One might want to separate the code by Django-app/Elixir application. However, I prefer to keep all the Elm code in one place and thus to keep the webpack config simple.
RESTful APIs over channels (sockets)
Again, in both frameworks, I’m using a RESTful API between the backend and the Elm code. That works neatly in Phoenix, which supports the full breadth of HTTP verbs. With Django there are limitations on the available verbs. For example, there’s neither PUT nor DELETE out of the box. So you need to improvise or use additional packages with Django.
With Phoenix I’ve tried out channels, which is an abstraction on top of web sockets. There are (abandoned 🤷♂️) bindings in Elm for that. However, for my taste there is still too much coding overhead. At least for my use cases the benefit did not justify the added complexity and extra effort.
Things I like
Let’s quickly go through the best parts of Elm. I am not going into details here, because I’d just be parroting the usual pitch. You can read about the details in others places.
Often Elm forces you to do things in a seemingly convoluted and complicated way. What I’ve realized after a while is that Elm actually only exposes the inherent complexity of the underlying problem. Elm forces you to face the complexity head on.
Furthermore, Elm provides you the tools to cope with that complexity. For example, decoding JSON values in Elm initially may seem overly complicated. Here’s an example where we decode a user struct with a decode pipeline.
In Elm you need to decode the response first, explicitly defining every expected field. But once you’ve defined the decoder you’re good to go. Either you can decode the value as expected or you’ll get an error response, which you will be nudged to take care of explicitly. So things in Elm aren’t really more convoluted or complicated, but actually simpler; maybe one could call Elm’s approach more “honest”.
The language itself
Pipelining (mostly `|>`) is another great concept, which allows to chain function calls, without nesting them and without having to name intermediate results. Once you know pipelines, you’ll really miss them in other languages, such as python (pipelines for PyTorch would be magnificent, 🤔).
“I call it my billion-dollar mistake. It was the invention of the null reference in 1965.” — Tony Hoare
The compiler is great. The error messages help a lot. The type system in combination with compiler messages allows you to do big refactorings without a headache. The compiler is fast, which is especially important and convenient during development.
The resulting asset is small and executes very quickly. Especially, when deploying for mobile it’s great to have a small build that doesn’t lag during usage.
The new time travel debugger can be of great help. The debugger keeps a history of all actions (received messages) with their resulting state. You can then jump through all historical state changes while debugging.
Finally, Elm pushes you to stretch your brain. Elm is not Haskell, but if you’re only used to object oriented programming or imperative programming, you’re going to dive into a different world that you didn’t know existed. In the beginning the concepts will feel awkward, but after a while they’ll feel like second nature and you will probably prefer the functional programming style for many tasks over the imperative or OO style.
The Elm Architecture (TEA) 🍵
The Elm Architecture defines how you can structure your app. This pattern is used throughout the Elm ecosphere. Its use is not mandatory, but in the end seemingly most projects do apply it.
Because most projects use the same basic architecture, it’s quite easy to find your way around other people’s code. Still, there’s plenty of flexibility baked into the architecture. For example, the update function can be changed to include additional arguments, such as a random value, the current time, an authentication token or a Navigation.Key.
Basically the same architecture is used for single pages (akin to components), for embedded apps, and even for single page applications. Kind of fractal 💥.
These days elm-ui is my go-to package for the UI. Actually, alone elm-ui provides enough value to me, to use Elm in the first place.
Another great tool I found useful is elm-parser, which allows you to create simple parsers. I’m using them for complex form validation (e.g. validating a date input in a free-form text field), and to parse complex values in
Of course, there are plenty more packages for all kinds of use cases.
Blissful everyday use
On a day-to-day basis, Elm is just fun to work with. When you write and deploy your code, you can be reasonably sure that the frontend will work properly. When you or your collaborators change some code, again you can be quite sure that you don’t break something else. Finally, large refactorings are quite relaxed. They still might take some time, but you won’t have the constant feeling that you just jumped into the abyss and there’s just no way out of it. Often enough I’m doing refactorings in Elm that I wouldn’t even think about touching in Python, or for the most part, even in Elixir.
Finally, Elm is fun to use on a daily basis. Even when projects grow I can make good progress without running into complexity walls.
Things I dislike
Nothing is perfect, so there are a couple of things about Elm that don’t suit me that well. Keep in mind that the benefits and downsides are not static. For me this list changed considerably during the last two years. A novice has different requirements than an advanced user. Also, before you read this remember that Elm is my overall favourite language.
Missing advanced language features
My first, and probably biggest complaint, is about advanced languages features, especially abstractions. Many people complain about those, but I don’t see them as a showstopper. Also, they were no issue for me until I was becoming an advanced user. I would like to have typeclasses, more advanced functions on lists, dicts, and functions (
is gone 🙁), pattern matching on function signatures (with fall-through), and guards. I understand that these features are mostly missing to keep the language simple and consistent. Especially for new users that’s a good choice. My guess is that actually the lack of those features might be the reason why Elm is more popular than PureScript, which is rather similar to Haskell and which has more advanced features.
Getting a random value, the time etc.
Some things are surprisingly cumbersome in Elm. These issues pop up because Elm has no side-effects. For example, getting the time or a random value can be surprisingly convoluted.
A pure function cannot produce a random value or the current time itself, since side-effects are required for that. If you need the current time somewhere deep down in a function, you need to first send out a command to the runtime, then you get a message with your value, then you need to write a handler / case in your update function, and then you can go on with your business. This whole process can force you to change a lot of function signatures on the way.
React has nicer themes. On sites such as theme forrest, you’ll find plenty of themes with React components. You can still use those themes in Elm, but you’ll have to write the Elm integration (at least a couple of wrappers/renderers) yourself.
Material-UI is really impressive. There’s nothing as impressive as material UI (react) for Elm. Elm-mdc, elm-bootstrap are great and will suffice for very most applications. However, material UI is just more polished, including its animations and page transitions.
Hard to find bugs
Some bugs can be hard to find. I usually debug with log statements. Logging in Elm is relatively inconvenient:
let _ = Debug.log “log name” “foo” in ...
. Granted, it’s a lot worse in Haskell. Another good approach for debugging in other languages is a step-debugger. Well, there’s none for Elm 🤷♂️.
Recursive imports can be a nuisance, because they are not allowed in Elm. Recursive imports can be hard to avoid sometimes because reality is messy at times. You can easily end up with weird references between data structures. Although a nuisance, this issue shouldn’t appear to often and there are ways around it.
Scaling the architecture (🤔🤷♂️)
Although I’ve written plenty of Elm code and although I had to scale my applications again and again, I still don’t know how to do it properly.
I try to stick to writing modules around data structure. This approach sometimes leads to files that are a couple thousand of lines long. I’ve got
functions that span hundreds of lines. Although it’s kind of discouraged, I often separate parts out using the page pattern. However, creating a page that can keep track of its own state involves quite some overhead. You’re practically writing another app, which is embedded in your main app. However, writing a page without its own state often doesn’t seem a good choice to me, mostly because it blows up the code of the surrounding module.
Things I dislike even more
Setting up Elm and issues due to missing abstractions bother me most.
The first issue has not too much to do with Elm. But setting up Elm with webpack to work in Django or Phoenix gets me every time. For me it takes a lot of fiddling until the setup works properly.
While some missing abstractions only lead to some more typing, there also are worse cases. As an example, when you want to restrict the keys to your
you need to use a
Comparables are defined by Elm and you can’t implement a
type yourself. That means you can’t just use an algebraic data type as a key in a
. In Haskell that’s resolved in a nicer way.
You define your ADT and derive
and you can use your new datatype as a key in a
. So in Elm you either lose type safety over those values (by using strings directly) or you have to do some awkward manual conversion between ADT values and strings.
Besides that I’m going to experiment with expanding the Elm experience to the server by developing a backend with Haskell. End-to-end type safety from Postgres to the browser sounds very appealing to me.
Actually, Elm might be the best pitch for Haskell for web developers!
If you like this article, please follow me on Twitter (@chronz_peter). I’m posting my experiences with Elm, Elixir, Haskell, and PyTorch.