The Missing Bit

How Elm saved my life

While the title of this post seems to be a clickbait or an over exaggeration of how a programming language can affect one's life, it is not wrong. It is not the sole factor, more a piece of a lot of little changes in my life, but it was an important one.

Here is the story of how Elm helped me out of burn out and back to programming with a smile.

I guess this is the "my life story" post, so I have to provide a bit of background to who I am and what I do. It is very personal and quite long. There is a TLD'R at the bottom if you are in a hurry.

Since 2002, I am a freelancer, I have my own company, but in practice, except for some project and office work, I work alone. I started doing IT, in 2004 I launched a datacenter project in my parent farm to host my IT customer backups, it was dropbox before dropbox (using rync and clever scripting). As side project, I did game programing, I wrote 5 "toy" game engines over the years and I always enjoyed how game worked as state machines. I always wanted to write my own game, but never had the time to actually do it. I realize now it was an excuse because I was scared of player judgement, but that is how it felt at the time. Anyway, this little background to say that I am a "backend and game engine where you have to do everything under 16ms" kind of guy. I love low level, I love to understand how it works.

Fast forward. I was in Australia in 2010, the experience was very mixed. We wanted to discover the country with my wife, instead we got stuck in an crowded apartment for months because of bad negotiation for the rent, we had no place to go and I had to pay upfront a lot of money for a single room. Anyway, the point is that when I came back home to Switzerland in 2011, I was exhausted and mostly lost all my IT customers. I hoped to be able to finish a game while being in Australia, but I had to do a lot of small works to pay for the daily life, I even wrote programming tests paid about 1$ per question.

Back home, I looked for work and found a position at a startup that was building a solution to manage smart Ad/info displays (from the next stop name in a bus to cinema schedules...). I started working with JQuery, and it was my first experience with the JavaScript ecosystem. Let's be honest, it was very messy. We were using some small "apple TV like" devices that had a JS engine and could render SVG. The devices weren't bad by themselves, but it was a closed environment with no JS debugger or any developer tools. Just upload and see what happens. I was used to the incredible portability and predictability of C, I know this sounds weird, saying C was portable and predictable, but it is. If you write ANSI C, you can compile it and run it on dozen of platforms with the exact same behaviour. I had a great deal of experience in C and knew how to write good portable code. Some of my network services wrote in 2002 were retired in 2015, 13 years of consistency. I can compile all my C code today on any system. The JavaScript experience was... different. Someone once said that French, which is my native language, was good for swearing, then: "C'était une chiasse sans nom, un merdier innommable qui puait du cul et qui m'a ruiné la santé.". Which roughly means that it wasn't a good experience.

It is hard to be objective about it, why was it bad? I mean, JavaScript is not that bad, it couldn't be alone responsible for all of it. The JS code was fragile, I had to rewrite the same code dozen of times, refactoring -> rewrite, adding feature -> rewrite, localization -> rewrite... Event processing, rendering, networking... all of it mixed in by the JS blender. I always did my best to avoid callback hell, to write modular code, nothing worked, after two week of changes, someone adding code there and there, and the code would break and had to be rewritten. We used Cappuccino (Objective-J, I even rewrote the compiler for our embedded system), SproutCore, Ember, Angular, JQuery, Knockout and other framework I don't remember.

In mid 2012, I was tasked to write a new frontend for a customer. I won't go into the details here, but it was impossible to iterate on the frontend with JS, it was only build something->show to customer->take feedback->rewrite. At some point I was alone on the whole project, responsible for customer relation, coding, documentation... At the end of 2012 I was very tired and weak, and I caught a virus called CMV which is usually benign but because of my state had an enormous impact on me. It started destroying my organs but my father, who is a doctor, was able to stop it, but the harm was done, I was apathetic for nearly 6 months, doing nothing having no energy or will to live. I had a kid in early 2012, and I think I would have killed myself if he wasn't there.

In 2013 I did a few iOS app, I enjoyed Objective-C and C, things were getting better for me. I started working with Memoways and I had to build a frontend for our video solution. We started with a native MacOS app but we rapidly had to adapt and move to the web to support more platforms and integrate our solution with the web actors (twitter, facebook, youtube...). I had to do JS again. I was not impressed by the evolutions, grunt and gulp were glorified makefiles. Production builds would download npm packages in another order and would fail in a weird "cannot reproduce" fashion. I used React with redux and tried TypeScript to bring hope to the glass tower that was the JS ecosystem.

It wasn't pretty, but it worked, I built a frontend for our video app, but then came changes. Maintainability was hard, refactoring was impossible without rewriting the affected code. undefined would collide with null and produce random runtime exceptions. Each time I had to work on another project and came back to the JS one, it took me up to a week to get it to compile again. JS don't care about API compatibility, proper versioning, contained side effects.

I was fed up, and tired, I lost sleep again and regretted working on a web app again. I tried to go back to mobile apps, but iOS only wasn't working anymore, you had to support Android, and customers were not paying anymore for mobile dev as a gazillion of "generate your mobile app with a single click" solutions appeared.

Elm appearance

In 2016, I saw a post about elm 0.17, and I was intrigued. I had already heard of elm in the past, but never really understood what it was. I was using Elixir for my backend and had experimented with ClosureScript, so the functionally programming bit intrigued me. I looked at it. And tried it. At first I didn't get it, it felt limited. I pondered a bit and went back to my TypeScript/React/Redux code. A few months later, I saw the release of elm 0.18. Then it struck me. I looked at my redux driven code and it was there, the elm architecture, a model being updated by message rendering a view. Except that the TypeScript/JS code was full of side effects, of possible undefined of forgotten branch handling, of possible impossible state...

At this moment, I started using elm. I couldn't work on Memoways video frontend anymore as we had no funds so I started another project. In a few months, I built one of the biggest app I've ever written. It is an app to manage medical data, imagine huge forms having a million possible layouts. But elm data modeling was making it a breeze.

My time with elm over the last year was the most positive programming experience I had in my life.

A few points about Elm:

I was able to get confidence back from my work. My oldest code has been in production for 8 months, no runtime exception, no bug, no time lost when adding features, no need to write a billion integration tests, it just works.

This post was mostly personal and emotional, but I still want to give you an objective, if possible, review of Elm. I will go into the details on how and why it is great for web dev.

Data modeling

Using regular JavaScript, it is hard to ensure no impossible state can exist.

Let's take a very simple example, take a search form, a submit button and a result list.

In JavaScript you might have the following data structure:


{
  results: [],
  query: "",
  searching: false
  failure: false
}

This is the initial state. Now, with this structure, you can have the following:


{
  results: [1, 2, 3],
  query: "test",
  searching: true,
  failure: false
}

Searching is in progress, but there is some results, are those the results for the previous query? The current one? Some cache? Can failure and searching be set to true at the same time?

Elm let you define exactly the data structure you need with union type.

For example:


type Results
    = NotAsked
    | Loading
    | Failure String
    | Success (List Int)

type alias Model = {
    query: String,
    results: Results
}

This model prevents impossible states, like searching and loading being true at the same time.

I lack imagination, so I took this example from this blog post.

Of course, the best person to speak about that is Evan himself, I encourage you to view this video on this topic.

When your entire app is modeled properly and no impossible state can exist, it removes a lot of business logic dedicated to detect inconsistencies. You no longer have code like this:

// This wil break eventually, it's ugly and unsafe, but seen often
if (!response || !response.results || !response.results.length) {
  // no results
  return undefined
}

with elm you just check like this:


case results of
  Success numbers ->
    -- here, numbers is a list of int and results where fetched from the server
    -- you have all the context to render your view
  Failure error ->
    ...

Stable API

This is simply enforced by the package manager. If you change your function types (arguments, return value...), the package manager will bump the major version of your package.

Easy refactoring

This is hard to describe without actually living it. When you are working with elm, you change change huge chunks of code at once, and when it compile, it usually work. There is no "oops, this value is undefined, need to go back to code". There is a huge satisfaction after each refactoring, it compiles, it works, that's it.

Maintainable and readable code

The first time I saw elm, I said to myself "no, I'm never going to write code with significant whitespaces, it will be unreadable and will break". But with the help of the compiler and elm-format, it never breaks. As elm is very simple and easy to reason with, it makes the code readable even without "boundaries" (brackets, do/end, ...).

Maintainability is really good in elm because of the compiler help and the library stability. Adding features is easy as well because you can just write your function type signature and fill them.

Best compiler error messages

If you have a typo (imagine in js, you need to load the app, try the feature and see the exception in the console):

288|                     (if lading then
                             ^^^^^^
Maybe you want one of the following?

    loading
    El.padding
    Html.Attributes.lang
    RemoteData.Loading

Detected errors in 1 module.

I won't list all cool error messages, but they are really helpful, link to the doc when possible and will help you move forward with no frustration.

Good libraries

I'm unsure if it's a side effect of the language itself, or of the developer "type" writing them, but libraries don't "break", they just work and have sane APIs.

A few of them I use:

The whole list on elm packages. Also elm search is awesome as it let you search by function type signature.

Enforced semantic versioning

It's great, no more breakage like after a new npm install. It gives meaning back to semantic versioning.

Great community

The slack community is great, there is a discourse forum too.