https://maddiem4.cc/blog/fuller-stack/ffff-immutability-as-maintenance-doctrine
Immutability as a Maintenance Doctrine
Lately, I’ve been looking to rebuild my open-source portfolio after a couple years working in private industry. One of the first projects that came to my mind to work on was Matplotlib1, and I’ve been tinkering away at it for over a week now. It’s exactly the kind of challenge I enjoy sinking my teeth into: a complex codebase with a long history, and an almost limitless value to institutional knowledge.
So far, a couple lessons have been time-expensive to learn, but one-liners to share with peers:
I’ll probably make several posts about my experience as I go, but something that’s come up so far is an idea that I’ve been dancing around for a long time, and slowly gotten better at articulating. It has everything to do with why the premier open-source scientific chart software is still using a vendored copy of render software from (based on the copyright notice) 2005, and thriving.
The gang takes a portal to the year 2005.
Going against the grain
For anyone who’s worked in (or with) the software industry, it’s exceptionally rare to see software become “finished” - complete forever, no further changes needed. I’ve heard plenty of people call the completion of software impossible, although some arguable exceptions like Donald Knuth’s TeX2 exist.
That hasn’t stopped us from trying to figure out the problem of software longevity, but the few success stories we do have, don’t tend to make longevity an explicit goal, or follow any of the consultant industry advice for how to get there. Anti-Grain Geometry is, I would say, one of those success stories. You can tell it wasn’t trying for immortality, the FAQ still confesses to be under construction 20 years later. But you know what is documented? The design methodology.
You know me, you know what I’m going to say: that you should read the whole thing for yourself. So having got that out of the way, here are the key takeaways I think are relevant to longevity.
- It operates in-memory without attaching itself to any platform-specific code.
- It requires almost no dependencies: just a handful of C functions and C++’s template system.
- It’s trying to be OOP, but in such an “early days of OOP” way that it avoids some of the worst bad ideas and ends up halfway to what we’d probably call Data-Oriented Design today.3
- Building abstractions through a numerous series of tiny baby steps which are each optional, but unimpeachably simple.
I could write a post expanding on each of these attributes, but I want to focus on the last one today. AGG doesn’t make a goal of being everything that anyone could want. Instead, the goal is to provide rock-solid tiny pieces that never need to change, because you can change how you put the pieces together, and provide some yourself.
Writing code with AGG tends to be something like:
// 3 bytes per pixel: R, G, B
unsigned char buf[w*h*3];
agg::rendering_buffer rbuf(buf, w, h, w*3);
agg::pixfmt_rgb24 pixf(rbuf);
agg::renderer_base<agg::pixfmt_rgb24> rbase(pixf);
rbase.clear(agg::rgba8(0.5, 0.6, 0.7));
Even without knowing much about what each of these layers of functionality do, you can kinda follow the idea that each of these lines builds on the previous one, adding more out-of-the-box stuff you can do, and more specificity. But if you decided at any one of those lines, “I don’t want to use the AGG abstraction for this,” you could just… not. Use memset instead of rbase.clear, why don'tcha, it’s a free country!
“Where do I put my feet?” “…Wherever you want.”
But Maddie, what does that have to do with software longevity?
Well, first of all, it means that the lowest-level abstractions are also pretty universal. Even if some high-level tool isn’t useful to you, the lower-level stuff (with its fewer assumptions) probably will be. With fine-grained abstraction layers, this cutoff can be as precise as it is personal.
Secondly, it means that end-users are free to compose the tiny pieces in novel ways, and integrate their own. If you need to invent some niche pixel format that AGG doesn’t support natively, you can make it yourself and slot it right in.
But thirdly: it means that each piece, individually, gets to have its requirements set in stone forever. All agg::rendering_buffer really does is hold a buffer pointer and some size metadata. But that’s all it’s ever going to have to do. That’s all agg::rendering_buffer is for.
All three points are important, but it’s that last one that should catch the attention of anybody who’s dealt with software lifecycles. Requirements changes are the main treadmill causing software to constantly churn and never be done. Small, evergreen abstractions with locked-down requirements pull the power cable on that treadmill.
Immutability: a brief refresher
I write for a wide audience, so if you’re not a programmer, here’s how immutable data works. It’s data that can be created, or forgotten when it’s not needed anymore, but can’t be modified.
On its own, that actually sounds less useful than data that can be modified (AKA, mutable data). But think of it like a road in a school zone: it’s more useful with a low speed limit than without. Constraints can be features.
If you’ve ever used git, you’ve probably noticed that commits can’t really be modified. If you make a new, slightly-tweaked copy (like with git commit –amend), it will have a different hash - it will be a different commit that’s just very similar to the original. This makes it a lot easier to do things like validate the whole history of the repository, cryptographically sign commits, and efficiently synchronize data between machines.
Of course, it’s hard for a purely immutable system to be useful in a practical way to humans, so you’ll usually have some outer layer of mutability on top. In the case of git, that’s branches. Branches are just a human-readable name that points to a commit hash… but which hash? Well that’s allowed to change, so even though commit f04cc128 will always and forever be one thing, main can be updated a hundred times a day. This pattern (immutable data, mutable references into it) is very common, very powerful, kind of a buddy cop dynamic in the programming world. These two might be opposites in a lot of ways, but when they work together, they’re a force to be reckoned with.
Rethinking longevity
We’re used to thinking of data in terms of mutability and immutability, but what happens when we think of code that way too?
Well let’s bring back the buddy cop dynamic. Your software is going to divide into two categories.
- Concrete things that will never need to change, but merely come in and out of relevance. This is your immutable code.4
- Glue logic that orchestrates those simple pieces into a complete, user-friendly product. That’s your mutable code.
Most existing code in this world is not intentionally divided along this line, and I would argue that this explains why so much software struggles with life cycle issues. But the funny thing is, this distinction is so useful that it’s been unintentionally reinvented time after time for practical reasons.
Have you ever heard the good word of feature flags? Yeah, they’re awesome. Let’s say you have some new version of your homepage. If you enable that new version with a feature flag, it lets a handful of QA people exercise the new homepage on production, it lets you sanity check performance, user behavior metrics and edge cases with a slow rollout, and it lets you roll back to the old homepage quickly if you have to.
“Feature flags are badass!” - Frank Reynolds, if he interned for a year as a full stack developer
Here’s how they work:
- Don’t mess with your old homepage. It’s set in stone.
- Make a new homepage. It’s separate code, even if it starts as a copy and paste of the old homepage.
- Both homepages coexist on your web server for awhile, with some logic for when to use each.
- When your old homepage isn’t used at all anymore, you can just delete it.
That’s literally just using mutable outer glue to pick between two immutable implementations. When you start looking, you really do see this pattern all over the place, especially in the ops/DevOps space. And that’s with no real intentionality, no thinking in these mutable/immutable terms, it’s just the thing that works (and that evolution converges on).
With intentionality, there’s a clear incentive to make more of your code unambiguously immutable or unambiguously mutable, and to try to make the latter part small. Modifying existing code is obviously possible, but it tends to be hard to do correctly, since you have to have a mental model of “who’s using this code, and how would my changes affect them.”
Immutability tends to cut through these Gordian Knots with a sharp edge. You make a new thing, incrementally adopt it, and remove the old thing when its use count drops to zero. That’s powerful! That keeps you from getting mired in hard problems, so you can deliver features faster, clean up dead code, and pull off architectural shifts that would be hopeless otherwise. No code will hold you hostage, because you’ve reserved your ability to delete it if it crosses you.
“Never isn’t the right word, because… I could. I might. I probably will.” - Dennis Reynolds
Basically, when people (rightfully) say not to do a full rewrite of a codebase, just incrementally reform it until it’s good… this is one of your strongest tools to make that happen, on time and under budget, avoiding crunch hours and firefighting stress. Code immutability is for programmers who like to retain their hair.
It’s a two-parter
I’m planning to do a follow-up post to this in a little while, covering my process of making a real pull request to matplotlib, and how the immutable approach makes that process easier. As much as I enjoy writing this blog, I do need to get back to that PR and get it into a submission-worthy state. I think part 2 will be important when it’s ready, because most of the value of this immutability type of thinking is actually in the small daily life decisions, rather than the 30,000ft architecture astronaut space I covered here. So if you liked this, you’ll probably enjoy the sequel even more.
Footnotes
- Honestly, while there’s a lot of valuable projects that could have made sense to pitch in on, MPL was front-of-mind for me after the Crabby Rathbun incident. That story ended up being an accidental advertisement for MPL as a project that takes onboarding seriously and has sensible boundaries around LLM usage. That’s worth supporting! In fact, so many other people had the same reaction that it’s been a little challenging finding unclaimed work I can tackle.
- Funny enough, the ticket I’m currently working on also relates to TeX!
- I’m going to praise AGG a lot in this post, but that’s not because I think it’s perfect. There are a lot of verbs expressed as nouns, and a lot of accessor methods. I think the former is why real code using AGG tends to have a block of scoped typedefs at the start of functions, and it’s just typedef after typedef until you say “I’ve had enough of this dude.” It also comes from an era before people put a lot of effort into making things const where possible (for correctness and performance), so integrating AGG with modern code can require casting away const in a way that feels sketchy sometimes.
- These pieces often have something about their implementation inside their name. Standard libraries are chock full of code like this, because they have an explicit mandate to provide stable building blocks. Unfortunately, stdlibs are usually pretty limited in their ability to influence or measure the usage of different classes/methods. So it makes sense why many stdlibs, like the C++ STL, tend to always accrete new immutable content but never clean it up later.