We've all heard the slogan by now. When it compiles, it works. And we know it's not true. I mean, people didn't lie when quoting this slogan, they were just talking about a very specific sense of the app being "working" - natural languages are very subtle; nevertheless, it is indeed a very practical metric when evaluating a programming language, or a framework built on top of it. There're many aspects of programming that humans are just not born to do well (sure, some are naturally better than others, or can be trained to do better), but can be reliably taken care of with good PL/compiler design, so it's obvious that this is the way to go. Just like how I gave up trying to do arithmetic in my head long time ago (doing it per se isn't the hardest part, it's how to make sure you've done it right).
Elm, Rust, they are among the best examples of PLs doing what they're best at. But of course, other kinds of bugs remain to be the responsibility of humans. So what are some of those bugs in Elm web apps?
Some logical ones:
- bugs as a result of lacking domain knowledge, or outdated domain knowledge
- bugs due to using the wrong source of truth
What do you mean by the wrong source of truth? I admit it shouldn't happen typically as the Elm Architecture explicitly promotes a single source of truth; but one example I've observed is that if there exists a secondary, transient state that tries to obtain its value only during a specific event (e.g. on initial page load), then in case it fails (e.g. due to timing issues such that the resource isn't ready at the time of the event), the transient state will have an invalid value. And if the UI solely depends on that secondary/transient state, things will go wrong.
Some more technical ones:
- bugs due to incomplete case-taking on non-union types
The default (_) case is really handy but sometimes I wish it weren't there (because it gives the false impression that you're using something better than if..else but actually you are not). Examples include matching a string, or a list. What's bad about this is the compiler can no longer provide helpful hints when you forget taking care of something, and this is particularly bad with Elm programming because you always think you can rely upon the compiler in this kind of places.
Maybe it helps if, instead of merely taking cases on a string (an example is mapping URL path to a Route union type), we do a full parsing ritual to be more explicit. But ultimately, it comes down to how meticulous and exhaustive we handle errors. Just don't ignore them, ideally, not a single one. At least make the error state observable in the UI. Logging would be nice too.
Speaking of which, a related malpractice is to lump a bunch of different errors under the same error-handling logic. This is not itself a bug, but it can make future bug hunting that much harder, because the same observed system behavior may be caused by a multitude of underlying problems.
One error, one distinct behavior, that's what we want. Debugging is all about discerning the culprit in the code from observable symptoms, so it's always good to have a one-to-one mapping (or "injective" in math terms) as we code things up in the first place!
(OK almost forgot to leave an example. This has been way too abstract for talking about very concrete matters, bugs! So one example that I encountered was about using page redirection as the response to handle several system problems, including invalid URL path, which is more subtle than the old-school way of display a 404 page, and invalid auth session data, plus other custom, context-specific redirection rules; because all those issues are mapped to the same observable behavior, when I saw it, I was drawn to investigate several suspects, except the real culprit, the most obvious one - URL parsing; it was lame, but I know the right move is to put aside the shameful feelings, and actually improve the system where the compiler can't help as much - by adding a unique visual cue that shows up only if an invalid path is visited.)
Not bugs, but "code smells":
- wetness
Perhaps because it's much easier to write correct code in Elm, we might be more susceptible to lazy habits of not keeping things DRY. I mean, it's not necessarily a good thing to dry things right away, in fact, that would be often premature; and simple repetitions are pretty harmless as well. But I did see one case where a workhorse-level, non-trivially complex piece of logic that got duplicated throughout the codebase, even including TODO parts carried over.
That shocked me and my first response was like, hey, maybe this complexity should be handled by the backend instead! But after my rant was heard, I was encouraged to give it a try and dry this thing up at the frontend first. It ended up working really well, actually it's one of the most satisfying moments since I joined the team, and I have Elm to thank for - yes, if the backend were to handle it instead, then we just wouldn't think about it, wetness is gone from our sight, but that's just pushing away responsibility; but with Elm, it's actually possible to get a decent frontend API by wrapping the same backend API with a good module interface, using custom types to abstract away things and only expose what's relevant. I ended up taking responsibilities, how empowering is that!