JoT
Note
coding

Trade-offs Made by Elm Users

2023-08

Huidong Yang

Due to schedule changes, I'm afraid that I won't be able to spend as much time on writing, and I have to make the switch to a less polished form of producing pieces, because that's the only way to keep it going. And reflection on a regular basis is essential to me.

I still get to write in Elm, and that's a huge plus. Those who get Elm will understand this perfectly. But there are significant differences. One-man project vs. team effort, that's the main change of constraint, and that of course leads to making different trade-offs.

For one-man projects, which I'd been exclusively working on until August, is a simplicity in Elm module hierarchy, keeping it as flat as possible, and basically means to work in as few files as I can by default, and only create new modules when there is a significant benefit in doing so.

But if we have a multi-person situation, then there will be some issues. The most obvious is a DX one, where Git merge conflicts will be a normal, as people will be very likely modifying the same files at the same time. Project management-wise it will also be less straightforward, because to be honest, we are still very much used to think in terms of modules or files, instead of dividing responsibility at the granularity of functions. But Evan in his talk "The Life of a File" clearly proposed a different approach to modularization in Elm, that is, start with growing a single file, use comments to organize functions within it, and only extract a separate module when you see a clear, well-defined data type (along with its role/behavior). But then Evan himself admitted that he didn't have as much real-life experience as Richard in managing industrial-strength front-end codebases. And then came Richard's "elm-spa-example". I think I mentioned this before, but moving from "elm-todomvc" as my first example to Richard's, was a shockingly steep curve, as the expectation had been set by Evan that the Elm Architecture (TEA) is all about minimalism, namely a foundation that is made as simple as possible (but not simpler); yet a supposedly textbook codebase is using Cmd.map and Html.map that are explicitly discouraged:

map : (a -> msg) -> Cmd a -> Cmd msg

Transform the messages produced by a command. Very similar to Html.map.

This is very rarely useful in well-structured Elm code, so definitely read the section on structure in the guide before reaching for this!

I still remember that comment in the code stating that, paraphrasing, "most of this (boilerplate) code will become unnecessary in a future version of Elm" (ref.1, ref.2). As much of a neat freak as I was, I immediately lost my appetite to dive into a boilerplate-riddled codebase that marked itself as quasi-obsolete.

I don't really mind complexity, heck, it's even part of nature. But unnecessary complexity, in the context of engineering, is a different story. As designers, people still strive for elegance, knowing that the world itself is full of flaws. I can understand why JavaScript frameworks tend to be complex. But the entire point of Elm is the take away much of the flexibility/freedom that caused all the nightmares in the first place (mutation, side effects, null/undefined, etc), so that we have a minimal set of building blocks that however you play with, will produce a predictable program in the end.

For JS frameworks, I get why all the boilerplate, because each is based on an idea how to write JS that produces nice apps. But Elm is itself a single idea (and in that sense, Elm is not just a language but also a framework... well, framework is a highly ambiguous term, what I mean specifically is of course the architectural aspect). That means, no matter how you devise the wire-up (boilerplate) of your Elm app, it always distills down to exactly the same thing (and highly skilled people either hate or love Elm for this exact reason). That means, in essence, there is no such thing as a "framework" in Elm, in the sense of JavaScript frameworks; however, at the same time, because Elm by designed is not plug-n-play with JS, what we need/lack is a rich ecosystem of toolkits, utilities, "libraries", "design systems", or what have you. And people are doing exactly this. And seeing that happening is a very inspiring thing. Elm is "quiet" nowadays, by normal JS standards, but it is very much alive and thriving. The same goes for Tcl. In fact, I believe these well-designed languages appear "quiet" exactly because real users are overall very happy using the language as is. No "riot" is needed (pun intended).

So is all the extra plumbing just wasted efforts, if eventually we all wind up back at TEA? Just like "zero-displacement" in physics term?

It's not that simple. Someone, maybe Richard, pointed that most JS people in the industry are simply not used to starting a project from scratch. For me, since so far, each of my projects was meant to be very different, almost orthogonal, in order for myself to learn a variety of things that I cared about, I did end up most often starting just from a counter app. I did however, just once, try to make a "starter template" kind of thing, but due to the exploratory nature of my personal interests, I never settled on a truly useful template. (And I love writing Elm, so each time I repeated myself, I was actually feeling quite zen.) But I can imagine and understand that such foundations to rapidly "bootstrap" a new project can be very pragmatic, and especially beneficial for a team that works for clients in the business/industry world, where what they need is (1) big and feature-complete, but (2) shares a common set of reusable "components".

So to sum up, extra plumbing/wire-up can be a pragmatic choice for project management, "division of labor", that kind of considerations, and being able to work on a disjoint set of files per individual most of the time is also a plus in DX. Being able to grow a reusable arsenal of in-house built components that carries on from one contract to the next is also super time-saving.

And it always boils down to making trade-offs, saying is almost feels like a cliche now, but time and again, I find this mental framework still relevant in actual programming.


And all these reflections above led to a realization: I never had to work for a client - in those projects of mine, I was the client. I make only what I want, and nothing I don't. And as a minimalistic guy, I only cared about a subset of things about a web app, but at the same time, I am very conscious about the fact that these apps that I find useful myself, are not ready for the real world. With Arrow, my first Elm app for personal time-tracking, doesn't have a mobile/responsive view, is offline-only and has no cloud-storage, no multi-device data sync. My apps don't have any industrial-strength visual effects like transitions/animations. Heck, I haven't built a standard RDBMS-backed client-server app since my school days! All because I was my own client. I wanted to use Redis for two of my learning projects (one is a Dynalist clone, the other a client-server chat app based on pubsub), so why bother using Postgres? But I know most clients will say no to Redis as the main database system, and the best argument is, and I 100% agree, that Redis doesn't have a query language, so fuck that. Redis is what I call a "lean and mean" tech, and it's a sharp tool with the usual pros and cons. You have to be extremely dedicated to it in order to produce an industrial-strength backend system by building on it alone. And what's RDBMS for? Exactly so that people don't have to pull those genius stunts to produce, for instance, just a run-of-the-mill e-commerce app. Yeah, this TigerBeetle thing I heard people talking about, is along the same line. You have to dedicate yourself to produce a perfect system (with some every exotic constraints), and obviously, this is not what your everyday client has in mind, most likely.


Recently I watched the web component talk by Luke. It's a good talk, no doubt, but at the same time, it shows, once again, how people differ in this eternal art of trade-off making. In the very beginning, he demonstrated what it was like to use CodeMirror in Elm using ports, and the obvious point was, phew, wiping sweat. Got to be a better way. And when he'd found out that there indeed was, mind blown. But to me (as of now of course), my gut feeling was very different. When he was calling those WebAPI calls for custom elements and stuff, I saw so much "extra plumbing" involved, whereas with the ports approach, it has exactly zero cognitive overload, because we all know ports. The only downside seems to be verbosity? And god, verbosity is the most lovely of all the drawbacks we can imagine in coding. (Note verbosity /= wetness). And there was this interesting question after the talk, about this experience of observing the fight between the CodeMirror instance wrapped as a custom element vs Elm's own runtime. Luke's solution was to implement input debouncing. I have no experience with CodeMirror, but my guess is with the ports approach, there would be no such fights. In general, my position since learning Elm has been to write as little JS as possible, and that doesn't necessarily mean lines of code, but more about architectural complexity. And writing those incoming and outgoing ports, i.e. message passing style, is the simplest thing I can imagine we do in JS, there's basically no "architecture", just functions that do something on some data. No matter how many lines of code you write for ports, the complexity of the code stays the same. In some way, this design of Elm ports is in the spirit of the ideal "plugin system", as in the extensions/addons of code editors and web browsers. A great design not only allows those plugins that users write to offer great utility, but probably more importantly, disallows them to harm the reliability and performance of the host system itself. I still remember back when Firefox finally decided to move away from their old, very powerful addon API to something much limited but safer, many people were outraged, including Quicksaver, who made the awesome addon "Tab Groups". And I thought that's the end of this truly impressive UI (since Firefox has no intention to build this feature in). But as the new API gradually became richer, and people got more used to the new mindset (around, surprise, message passing), the hero finally arrived. (But for the record, I'm nowadays quite disillusioned about the future of Firefox ever since they fired all the Rust people after they released "Firefox Quantum". I don't want to say it, but Chrome didn't beat Firefox, Firefox failed itself. And I digress, as usual.)

To me, this signifies a major conundrum of mine in coding/learning. On one hand, minimalism makes me super focused in design for the exactly problem at hand, which allows me to discard as many assumptions as possible regarding what needs to be in the solution, and that mentality is highly rewarding; on the other hand, that discourages me to just explore things around, trying new tools and different approaches, reading other people's code, etc. Again, a trade-off, but I'm now seeing that over-leaning on one side puts me in a disadvantage when it comes to industrial knowhow.

Ultimately, I'm not saying what I chose was plain wrong, I'm definitely not going to take a U-turn now and go the opposite direction. And that's one of the best things I found as I started working with other people recently. I got to finally get the "excuses" to try all those things that I never had the chance to, because of the minimalistic mindset.

And this is a very good thing, because I still believe the following, I'm just repeating for the second time here:

No experience -> no opinion; No opinion -> no new design.

And we all know that new designs is what moves things forward.


Finally, I wanted to say, Elm has, truly admirably, managed to settle on a fixed architecture that is "as simple as possible, but not simpler". I know all analogies eventually fall apart when you look closely enough, but I still want to draw this one: it's like the absoluteness of the speed of light in the theory of relativity. Once you have this MVU architecture, with a runtime to manage effects, then you actually gain a great amount of freedom and flexibility, just in very different dimensions as typical JS people like to think. You can either start a project with the counter app, or with a full-featured toolkit/framework/starter template. You can use ports for JS interop, or if you feel adventurous and webby, use web components. You can have as few files, and as flat of the module hierarchy, as you want, or the opposite, if you think that improves the overall productivity. People in the Elm community often say that there's so much they can do even without any new language features, and that's exactly because of all the guarantees Elm provides, and these guarantees obviously did not come for free, in a sense, Elm sacrificed its mass appeal (plug-n-play with JS world) for this, and those who get Elm will understand that it's all worth it. In a sense, what makes Elm great is not what Elm has, but what Elm explicitly refuse to have - its value is in what it subtracts from JS (duh, Elm compiles to just JS after all).