A look at Elm 0.19

I wrote my first line of Elm in 2016 and have remained fascinated by it (Elm, not the line 😅) to this day. There are many things to like about Elm, but to me they all converge at this: it brings a sense of coherence to the frontend development. Ever since adding Elm to my toolset, I stopped dreading browser programming. While still a backend developer by the day, I feel like I’m always tinkering with some Elm code on the side. Whether it’s a small single-page app for shopping lists or an ambitious attempt at an ECS-based game engine, or just throwing together a bunch of primitives in WebGL, I find it enjoyable and engaging.

Elm 0.19 was released almost six months ago, in August 2018. This version had been in development for almost two years. During that time, the language designer Evan Czaplicki did a complete overhaul of the parser and reinvented his code generation approach. All of that to reduce the resulting bundle size: a concern that prevented some bigger users from adopting Elm. As a casual user, I’m not worried about bundle size too much, but I can still appreciate the simplicity of the resulting solution. With Elm 0.19, if you run elm make with the --optimize flag, your bundle will be optimized. That’s all there is to it.

Other additions include the new elm.json project definition format and the elm binary that now contains all necessary tooling under a single umbrella. My favorite thing to show people new to Elm is how refreshingly easy it is to start a new project. You create a directory, run elm init in it and then you’re ready to code. No need for an index.html or a project file. Write some Elm, run elm make Module.elm and get a compiled HTML file or, even better, spin up the built-in Elm Reactor server (elm reactor) and trigger compilation by refreshing the page.

A bunch of quality of life improvements have been made to the Core and the concomitant libraries such as Random and Time. Among these, I’m particularly fond of the removal of Basics.toString in favor of more specialized String.toInt, String.toFloat, and Debug.toString. The former used to occasionally break the fundamental promise of the Elm compiler: warning you when’re no longer passing the same type of value to the same function. I’m not going to re-iterate the changelog, but I’d like to touch on the subject of backwards compatibility.

So far, every major Elm release has introduced some breaking changes. Evan is known for ruthlessly removing entire concepts from the language. Most often, these changes strike at remaining Haskell vestiges such as the special Range syntax ([0..9]) or ability to use any function as an operator (3 `plus` 5). Other times they might remove an ambiguity or even get rid of an unsuccessful API (like, websockets). It’s hard to avoid making a comparison with design principles upheld by Apple. Unsurprisingly, a common criticism of Elm is reminiscent of that of the Jobs’ company: it often sacrifices the wishes of power users for the sake of newcomers. However, unlike Apple, we get to experience Evan’s thought process in detail from his articles on the topic and conference talks.

With the latest iteration, Elm is saying farewell to user-defined operators and native modules. There was some controversy spurred by this decision, but Evan’s rationales make valid sense to me. I often wish that certain Haskell libraries used human-readable names in place of custom operators, and I can see how supporting native modules can eat away at the maintainer’s limited resources.

I highly recommend watching Evan’s talk “The Hard Parts of Open Source”, which describes the challenges faced by maintainers of popular open-source projects and explores the nature of certain toxic dynamics within online communities, and finally, suggests some potential ways to alleviate them.

I wouldn’t write this post without spending a considerable amount of time with the new version of the language. Even though upgrading an existing project would’ve been an efficient way of getting up to speed on 0.19, I’d decided to take on a new challenge instead and implemented a generator of The Witness-eque maze panels. You can read about the algorithms behind it in my previous post, while I’ll dedicate the rest of this article to my impressions from the language.

If I have to pick one great thing about programming in Elm, it would be no runtime errors. Well, aside from the two or three times when I blew the stack when trying to traverse a graph without marking nodes as visited. And another time when I triggered a browser-crashing barrage of warnings when I attempted to compare two functions with == on a hot code path (it seemed to work ok though 🤷‍♂️). I mean, overall, Elm delivers on the “if it compiles, it works” promise. The process feels almost like a video game loop where every successful compilation leads to feeling invincible and smart which leads to wanting to continue to code (spoiler alert: this is how you stay up until 2 a.m. programming maze generators).

The second big thing is refactoring. If you look at the commit history of my maze generator, you’ll see numerous big refactorings (like, the one where I extracted the quadgraph implementation into a separate namespace). In every case, I’d move some code around and then follow the compiler errors until everything compiles again, and then the game would “just work”. Only once I managed to break the logic by doing that, and that was totally my fault.

Last but not least, describing multi-step data transformations with the |> operator feels just as enjoyable as Clojure’s threading macros, but not as scary. Personally, I’m never fully comfortable changing a pipeline like the one below in a dynamically typed language without a bunch of tests, but it feels perfectly safe in Elm.

vertexDistances
    |> Dict.toList
    |> List.sortBy (Tuple.second >> List.length >> min minAcceptableLength)
    |> List.reverse
    |> List.map Tuple.second
    |> List.filter (\solution -> solution |> List.head |> Maybe.andThen (QuadGraph.get graph >> Maybe.map QuadGraph.isLeaf) |> Maybe.withDefault False)
    |> List.head
    |> Maybe.withDefault []

I’ve also faced some minor issues. For one, Elm’s purity occasionally makes debugging a little difficult or requires one to take a longer route to get to some values. Elm provides an “escape hatch” in the Debug.log function that can be used to print a value to the browser console (essentially, perform a side-effect). However, while working on a game, on several occasions I wished the language would allow me to temporarily couple logic with rendering (e.g., draw some debug output to the canvas from the graph traversal function).

Also, more than once I was caught by the fact that Debug.log accepts two arguments (a label and a value), when I only provided one. A program like the one below compiles but doesn’t print anything:

concat x y =
  let
    result = x ++ y
    _ = Debug.log result
  in
  result

I’ve also encountered a bug in the compiler when it would crash with some cryptic Haskell-flavored message and no compilation errors. I was able to work around that by removing parts of the source file until I narrowed down the source of the error (something with an import statement in one of the files). In hindsight, I should’ve saved the state of the code and reported the issue, but that thought didn’t occurr to me until much later and I couldn’t remember what exactly caused the problem. Chances are, no one will ever see it.

Lastly, although not really an Elm issue, but it seems like at some point you inevitably run into the complexities of the larger web platform. For example, I’m using SVG to render the maze. I initially used the vmin units for SVG coordinates, but quickly found out that it only works well in Google Chrome, and had to take a different approach. I later ran into similar problems with SVG animations, which I never quite figured. Thus, the path flashes red only the first time the player runs into an obstacle. And there’s also this inexplicable glitch when only a part of the path blinks.

Overall, I had a ton of fun with this project and I’ll certainly be doing more Elm in the future. And if you, the reader, want to build something cool for the browser, remember that Elm is there for you.