This is a post about two experiences I had using Elm. It is quite long, written primarily for the sake of other people who might be considering using Elm — the things that might trip you up etc. I have put quite a lot of content into footnotes for the sake of brevity, so you are advised not to read footnotes unless you want the gory details.

At the very end, however, it gets derailed as I attempt to address what I see as a potential critical weakness in Elm or the Elm community, triggered by some recent events. I tried to separate out these two things into separate posts, but realised that they actually stand together. If you are thinking of using Elm, both parts are relevant.

An upfront summary:

Where Elm is good, it is really, really good. It enables some things that are so hard in other languages you might not even imagine attempting them.

There are significant downsides to Elm. Some of these will probably apply to other frameworks with similar architecture, like React+Redux in some of its forms. Some are peculiar to Elm.

Therefore, you should be aware of these before you start to use Elm, and be convinced that Elm really does solve the problems you have if you are going to persevere.

Sometimes, you should not use Elm. You can use JQuery/VanillaJS , and do so without embarrassment or apology. It is the Right Solution to some problems.

Don’t be afraid to use Elm with interop of various sorts. Let Elm be good at what it is good at (which is not everything), it’s easy to use interop where you need it.

Ahem, oops, cancel that last point. The Elm developers have a different opinion to what I express, and are not afraid to attempt to impose their opinion on anyone using the Elm compiler. Hopefully this is just a growing phase for the Elm community, and a more respectful and mature attitude will prevail. [I will happily update this post if there is significant change on this front]

To summarise the summary: Should you use Elm? You are an adult, you get to weigh up the risks and decide.

Project 1 I’m not at liberty to give many details about the first project, but it was a small web app, with a few typical things (user registration and login etc.), and then a few CRUD like pages. It was a requirement that all operations should be done with AJAX/REST backend, without the need for page refreshing. I chose Django and Django REST framework for the backend, which worked great for this project, there was so much out-of-the-box functionality it felt like cheating. I decided on Elm for the frontend, partly as a chance to upskill and learn a modern frontend framework. I had played with Elm before, and done enough programming in Haskell to not be put off by the functional programming side, so this seemed like a far better option than React etc. I tried to follow this Single Page Application as an example, which was very helpful, and I’ll use it to illustrate the things I mention. My first impression was that for some things there is an awful lot of boilerplate. Wiring everything so that messages get passed to the right place, and JSON decoders to convert all incoming data to something better to work with etc. took a really long time. JSON decoders are a particularly nasty hurdle to get over. They are almost unavoidable if you are receiving data from your backend, which means you will hit them quite early on in a project, they are very tedious/boilerplatey, and they are also quite tricky to write in some cases, due to working at a higher level of abstraction than you might be used to. In addition, the equivalent code in a JQuery/VanillaJS project is basically non-existent (you can just receive a JSON object and start using it). There are of course big benefits to the way that data is marshalled into your Elm code, but if you are new to Elm I imagine this could be quite off-putting. Handling of forms was also very discouraging. If I had been doing server side rendering of a form, I could have done it using extremely little code in many cases (Django will take your model and generate a form for you that renders itself to the right HTML and handles input). Writing out the form HTML in Elm seemed to be very tedious, not to mention un-DRY. Worse, when you’ve got the HTML done to the point where it looks right, in Elm you have a form that is far less functional than you might expect. For every field, you will then have to explicitly add a field to the model, a message to indicate the field was a changed, an onInput handler to the form HTML, and then add a branch to your update function to implement recording the data into the model. This is just to get you to the point of “a form that can remember what it has in it”, before you think about doing anything with the form data (like sending it over an HTTP API). This is a lot of work. However, the bigger thought in my mind was “this is just the Wrong Thing”. HTML and browsers can already do forms really well. They have already wrapped up that whole “allow someone to type something in a box and remember what they typed, and give it a name” thing into a component called an input box, so you don’t have to re-implement it every single time, you just type <input type="text" name="myfield"> . The built-in functionality of browsers plus HTML is getting better, too — you can do more and more things like client side validation declaratively. Why am I throwing all that away? In the end I was 500 lines of code and at least 10 hours in. I had some basic page ‘routing’ set up, and I had a login form that mysteriously did not work (I did not stay logged in), and I was out of my depth in terms of debugging it. At this point, with limited time allowance, I decided to cut my losses and abandon Elm. I was able to keep the backend stuff I had already implemented, and switched to JQuery and TypeScript for the frontend. I had never used TypeScript before, but it worked really well. With strictness settings turned up to full, it added both a great layer of safety and lots of productivity enhancements in terms of auto-completion (using tide). For login/password reset etc., I was then able to leverage server-side rendering of forms with just enough TypeScript to get it to submit via AJAX. For other pages I did more client-side rendering. Certainly there were points where the classic JQuery/VanillaJS approach of mutating the DOM brought with it the usual issues of needing to be careful to get the state synced etc., but it wasn’t too bad, and it was much, much faster to write. I was sad that I hadn’t got to learn Elm, but it was definitely the right decision for that project given the time constraints.

Project 2 I am not that easily defeated, and I had another chance to use Elm. This was for a side project of mine, LearnScripture.net. Since I do it in my spare time, there were no time limits for this project. There was one page of the project which had become a nightmare to maintain. It was the ‘learning’ page, which accounts for about 80% of the time that users will spend on the site, so it is right at the heart of the app, although it is a small fraction of the actual code. While most of the pages were and are server-side rendered using Django, with a smattering of Javascript, this page was heavy Javascript, implemented using classic JQuery style (hiding/showing nodes, changing the contents of some). Some of the state was stored implicitly in the DOM, some in extra module level variables. I wanted to make sweeping changes to the UI of this page for various reasons, but was afraid to touch the Javascript, with good reason. I had already introduced a set of set-up/tear-down functions for various things that manipulated the DOM into different states, but I was aware that there were still bugs. I figured this was a project that was right up Elm’s street in terms of managing the state. It was also going to be demanding — there were certain interactions that were intrinsically quite fragile and getting them right in Elm might still be hard (this proved to be correct — more on this later). However, there were no classic HTML forms on this page, or other functionality where I thought “the browser/HTML gets me most of the way there, I just need a bit extra”. This time I was successful. It took me quite a few months working in spare time, and I had a transition period with some beta-testers who tried out the new version and gave feedback, which was essential to catching bugs (including some regressions on the fragile behaviour I mentioned before), and getting feedback on the new UI and iterating a bit. But the new version of the page is now live and the old code thankfully deleted. I’ll describe my experience in categories of things that are hard with Elm, things that are tedious or annoying, and where Elm really shines.

Hard or impossible with Elm Fine-grained DOM control Elm makes certain types of control of the DOM much harder. First I need to briefly describe a simplified version of what my code does. There is a lot of detail in this section, but it illustrates both some of the challenges and some of the amazing things about the Elm architecture. The page I rewrote in Elm presents a Bible verse to the user for learning. It visually breaks the verse down into words, then progressively hides the words to force you to recall them. Finally it prompts you to type each word (at least in one learning mode). This is done by placing an input box exactly over each word — looking something like this: The upshot (once you include all the corner cases I have not mentioned) is you need to: Update the DOM with the verse split into all the word ‘buttons’. Find the position and size of the word that is going to be tested. Adjust the input box position and dimensions to match this (accounting for padding etc.) At this point show the box if it was invisible before (remove display: none ) Focus the input box ( node.focus() ). This is hard in Elm, because you don’t get to control the modification of the DOM, it is done by the Elm runtime using the virtual DOM you have provided. You have to wait for the DOM to get updated before you can query it, so doing it all at once is not possible. In order to move the input box into position and make it the right size, it turned out that the easiest way was to use a port, but even that turned out quite hacky, because ports can run before the DOM is updated. Getting this right and working correctly across multiple browsers sunk a lot more time than in the previous Javascript version, due to the lack of explicit control over the DOM. There was a further complication. When you focus an input box, mobile browsers will bring up the on-screen keyboard, which is what we want. This works for a manual interaction with a touchscreen, and also if you call node.focus() . In my app, there are a bunch of buttons (“Next verse”, “Practice this verse again” etc.) which can move you into the ‘test’ phase you see in the screenshot above. These all worked fine in the previous version, with the keyboard appearing just as it should. However, the same was not true in the Elm version. This might not seem like a big deal, but in my app it was — people spend a lot of time in this interface, and as I tried it for my own usage I immediately noticed this regression, I had to manually focus the input box in order to be able to type. As well as being annoying for me as a user, it was even more annoying for me as the developer — I was not going to spend months rewriting a page just to end up with usability regressions. It turns out that the cause was this: in some browsers, calling node.focus() only brings up the keyboard if it is within an event handler corresponding to a user action, such as a tap/click. The Elm architecture relies on running code through requestAnimationFrame , and so it does not work. This problem affects both Elm’s Dom.focus command and all code running through ports. I eventually found a workaround. At the heart of it are two things: Within Elm, use the generated HTML to indicate the buttons that need to trigger focus. Outside of Elm, use normal JQuery to add an additional, normal event handler to the buttons that will trigger the focus in the required way. The complication in part 1 is that I had several buttons that needed to trigger the “show and focus the input box and show the on-screen keyboard behaviour”, and, worse, the situations in which they would do so were complex. Essentially, at the time I rendered each button, I needed to ask “supposing someone clicked this button, what would it do? Would it put us in a state where we now want the input box focussed and the keyboard to be present? If so, add certain attributes that the JQuery code can pick up on which tell it that focussing needs to be done, and tell it all the other bits of data needed to do the focussing”. In general, this seemed like a hard problem to solve reliably and generically if there is any complexity to your code. Can your code know at runtime exactly what clicking any button in your interface will do, without it actually being clicked? In the Elm architecture — yes you can! In fact, you already have exactly the function you need — it is called update . It takes the current model and a message, and returns the new model. And a message is exactly the thing that every button already has — when rendering a button you essentially add an onclick attribute that indicates the message it should send when clicked. All I had to do was bundle these up into a data structure that looked something like: type alias Button { caption : String , onClickMessage : Msg } and a render function with this type: renderButton : Model -> Button -> Html Msg In fact I already had both these, but renderButton had to be adapted to take the whole model (a little bit hacky, but it works). The renderButton function now literally calls the update function to find out what would happen if the button was clicked, and if necessary adds HTML attributes to the button to communicate to the non-Elm world the additional requirements. This may seem a slightly hollow victory — this code is only needed because of an Elm limitation — but it got me thinking. At first it seemed utterly magical that I could programmatically know exactly what clicking a button would do to my page before it was rendered. It then occurred to me that this is a really basic thing. Shouldn’t any interface we write have the ability to answer such a basic question about itself as “what will happen if I click this button”? Yet there seem to be extremely few frameworks that allow that, Elm being one of them. It is possible in Elm only because every function you write is pure, therefore you can run any function to get a speculative new state, without worrying about potential side effects. The above technique doesn’t include the side effects a button click might have — side effects are returned as the second part of the return value from the update function, but in a data structure that is opaque to you — Cmd values. However, if there were certain side effects you were interested in knowing about, you could make your update function log these into your model in some way, as well as sending them as commands. In this way, you could use the same technique to know exactly what side effects a button would trigger (e.g. sending an AJAX request) without triggering it. AJAX retry A second problem I had was implementing ‘retry’ for AJAX calls. This was something the previous version had, implemented using jquery-ajax-retry and a few very generic functions, and it had worked well. When an error occurred, or while retrying, these generic functions just reached into the DOM and displayed a visual indicator of what was going on. There was no explicit global state in my code — the necessary state was passed around either through the jQuery plugin or in closures. When it came to Elm, things worked very differently. In order to be able to retry, and to display messages about progress on the page, I had to explicitly save all the data into my model, and significantly re-structure the app to get everything to work. I could not find any generic, re-usable “AJAX retries” Elm package, and I can’t think how it would even work within the Elm architecture — Elm’s response objects don’t contain the data on the original request in order to enable retry. Since the different requests had different types in Elm, I couldn’t just put them all in a list — I had to create further union types to handle the data for each type of request. Figuring out things like adding delays etc. was all a lot harder and I tied myself in knots with Tasks and types at some points before I figured it all out. However, the resulting design turned out much nicer. Since I have explicitly captured all the state needed to send the HTTP requests, I can display much more information. I also now have a manual retry button (for when your internet connection goes completely), and in addition I can save all the requests to localStorage before sending, so that if there are serious internet problems, or the page gets closed (or has to be reloaded because of low memory on a mobile app), it doesn’t lose any data. I also realised that my previous code had various bugs in this department — for example, with the old, ‘easy’ retry code, it was entirely possible for requests to get sent out of order, which in some cases would result in bugs for my app. With the new design, this bug was also present, but fixing it was much easier — I already had a store containing all the outgoing requests data, I just had to turn it into a queue. Performance One of the points where my re-development nearly got derailed involved some performance sensitive code. I had no idea it was performance sensitive until I tried to recreate it in Elm. My app uses the Demerau Levenshtein Distance algorithm at one point, and this turned out to be hard to rewrite in Elm. All the optimized versions of the algorithm you can find depend heavily on random access/mutable variables/destructive updates. This kind of problem is not uncommon. With all pure functional program languages you will hit cases where it is hard or very hard to write efficient code. Both Elm and Haskell suffer from the problem that creation of intermediate data structures can cause a lot of overhead and harbour non-obvious allocations that cause your algorithm to have vastly worse performance than you expect (including changing O(n) code to O(n²), for example, or worse). Since this is a pure function, which I want to use synchronously, ports were also a very bad fit. Thankfully I eventually found “native modules”, and this helpful guide. Native modules are simply what the core Elm implementation and libraries themselves use when they needs to access Javascript functionality (e.g. XMLHttpRequest etc.). It was very easy to integrate my existing Javascript, and there are no downsides — this was a very well tested pure function that had years of use in production that convinced me it was correct and safe, and fast enough. Three hard problems, but three happy endings. (However, with the last one, there is a major caveat, which is the second half of this post).

Annoyances The purpose of this section is to prepare you for things that may wear you down if you attempt an Elm project, so that you are fore-warned. I am not a fan of writing HTML in Elm, but it works and the syntax is tolerable . There were a few points where the type system let me down by not being powerful enough . There were some times where the static typing rules reject perfectly valid programs , which will annoy you if like me you are used to doing a lot of programming in dynamically typed languages. Whichever way you look at it, there is often a lot of tedious boilerplate in Elm code — such as JSON decoders/encoders, enumerating all the messages your app sends and receives, wiring everything up in update functions. The result is you do write a lot of code. At the point where my rewrite had approximate feature parity with the old version, I looked at the stats in terms of lines of code: The old version: approx 2000 line of TypeScript / Javascript

approx 230 lines of HTML The new Elm version had: approx 4000 lines of Elm

approx 230 lines of Javascript

A basic 20 line HTML page. (blank lines excluded) And both had some CSS/LESS, of very similar size. Part of the reason for the large amount of Elm is that elm-format uses a lot of vertical space. Another reason would be that things like nested object updates in Elm are extremely verbose , and this also combines badly with the loss of context when handling a message, which is an annoyance in its own right . I imagine that some of these may be improved a lot in future. Some could be solved with code generation (e.g. if you have a strongly-typed backend system generating JSON it could also generate Elm decoders, I think there may be packages that do this already for some languages), but some of them will only see limited improvements due to fundamental things in Elm’s design. These are part of the cost of doing business in Elm. Finally, there are certain types of bugs that I found happened more with Elm than with other languages, including a certain category of state bugs .

Elm’s strengths Despite the annoyances, however, Elm’s type system in general is a tremendous asset. The record system is much more usable than Haskell’s, which is good. When it comes to making changes and refactoring, Elm is so much nicer than most other languages I have used. It does take a little while to adjust and get used to a slightly different flow. In Elm I generally make lots of very small changes, ensuring it compiles at each stage, because iteratively making lots of changes is easy and safe, while writing large bits of code that compiles first time is harder. The result is wonderful confidence in making changes and knowing that you haven’t broken anything. For a large part of the rewrite I did, I wrote no unit tests at all. I did have a set of previous integration tests for the previous incarnation of the page. Most of these applied to the new page with minimal changes, but I only bothered to actual use these very close to the end of the process. I was quite happy to make large changes and deploy them (to my beta testers) with only manual tests for the new things I had added. Being able to avoid large numbers of tests was really freeing, and I probably gained much more here than I lost to the tedious boilerplate-y code I mentioned above . A result of the Elm architecture is the time travelling debugger, which really is very cool. In fact, just being able to see the current state of the entire model is great. The Elm architecture can also enable things you might not even attempt in other languages. I wanted to have a “help tour” feature for this page, that takes the user through all the main UI elements (some of which are rather compressed or cryptic when you view them on a small screen, due to lack of captions, which was a deliberate choice to make room for other elements on the page). A help tour can also be useful for giving tips etc. on the way, and for welcoming new users. There were a number of additional ideals and constraints. First, I wanted to use the user’s current data as much as possible, rather than some random sample data. Second, I wanted the user to be able to re-run the tour at any point (and of course exit early). Third, to show certain parts of the UI, I’d need some fake data because otherwise certain things would be empty. As the tour progresses, this fake data changes. This means somehow keeping hold of the real data but viewing the fake data, and being able to make modifications to the fake data, without affecting the real model at all. Fourth, when in test mode, it’s vital that most of the buttons don’t work (otherwise you could end up triggering side-effectful code that could do an AJAX call, for example). However, certain side effects must happen — for example, if the user had triggered an AJAX call, then started the help tour, when the data comes in, it must be handled correctly, routed through the real model. This sounded quite risky to get right. I wanted to be absolutely sure that I had done it correctly, so lots of special cases for “help tour mode” that I just had to remember to add would be a big no-no. With the Elm architecture however, it turned out to be quite straightforward. You need to effectively have two copies of the model, which actually means you have an extra, optional one stored in the main one. (My first attempt gave me an error message about a recursive type, but it was a really nice error message, with a link to a page that explained exactly what I needed to do). In the view code, you simply pass the fake model through when the help tour is enabled, plus info about the help tour controls, otherwise you pass the normal model. The update function is a bit more complex. I implemented this using 3 update functions. The top level one either just dispatches to the ‘normal update’ or to the ‘help tour update’ function if the help tour is active. The ‘help tour update’ then has to make a decision about each message. Should it ignore it, pass it to the real model (using the ‘normal update’ function), pass it to the fake model, or to both? There were a few cases where it needed a bit of careful thought — e.g. window resize has to go to both models, and in some cases you might want to drop the side effects from one update but keep the model. However, since every message your app handles is enumerated into a single type, it is easy to be sure that you have handled every case. This worked 99% perfectly the first time I tried it (that is, as soon as I had implemented the split ‘update’ function and got it to compile). The remaining 1% was entirely cosmetic and easily fixed. Further, adding this help tour required very few changes to other parts of the code — it was very well contained. I simply wouldn’t have attempted it, or possibly not even thought of it, if I hadn’t been using the Elm architecture or something like it, or I would have quickly settled for much less in terms of how it should work. However, even for this, there were parts of it that would have been really hard or tedious to implement in Elm (e.g. finding the location of a certain DOM element and then drawing an animated loop around it). These were straightforward to implement in Javascript with ports, so that’s what I did.

The conclusion I guess my biggest lesson from this is that Elm is really great at some things, and yet it complements, not replaces, other techniques and languages. At the same time as doing the Elm re-implementation, I added some small functionality to another page on the site, using HTML in a template and JQuery. It was a button (in fact a set of buttons) with a simple API call. When the call completed successfully, the button was disabled and the caption changed to indicate success. As I was implementing it I was extremely relieved that I was not doing it in Elm, with all the overhead of enumerating messages, wiring everything up, JSON decoders etc. Elm is most verbose at the boundaries, and also most prone to failure at them (if you pass the wrong kind of JSON in, for example, there is not much Elm code can do). Since small bits of Elm code have a large boundary to volume ratio, the costs can very quickly outweigh the benefits. If you have a small amount of very localised state to manage, or cases where the browser is already 90% there, Elm can be the Wrong Thing. For these cases, I no longer consider JQuery/VanillaJS to be a poor man’s quick hack, it is a technically superior solution. Elm still has really basic bugs when it comes to simply getting a text input to work correctly. This is the result of re-implementing in Javascript what the browser can already do — you should avoid this if you can. Sometimes you don’t need a framework. But sometimes you do. The overall lesson is to not use Elm at all when it gives you little advantage, and when you do, be prepared to mix it with other languages.