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.