Abstractions are about Trust

There is a popular concept in my particular internet bubble known as "Locality of Behavior". It's a sort of backlash to "Separation of Concerns", "Single Responsibility Principle", and various other justifications for abstraction. The idea essentially is, when one reads a function, they shouldn't have to click through to five other functions, one interface definition, and one macro to understand what it does.

This is not the only argument against abstraction. Often when we write abstractions they seem to quickly become "brittle", in that we have to tweak them all the time to fit new problem spaces while retaining use in other places.

So today we hear calls against abstraction in general, or at the very least, to not abstract "prematurely". The truth is we have absolutely no way to escape relying on abstractions (we're not writing 1s and 0s after all), and that's completely fine. What's troublesome is we keep writing abstractions we can't trust and have to update all the time. The reason developers are calling for "Locality of Behavior" is because when one developer provides a function/interface/class/module etc and claims it "does x", we've been burned enough times to say "I don't believe you" or "okay, but I need x + y and trying to add y will break z" .

So how do we write abstractions that can be trusted and are adequately flexible? Here is the only successful method I've seen so far: Get your abstractions from a trusted authority.

"What? What does that even mean? I'm a responsible developer. I don't trust my dependencies. I audit them and remove them when possible."

I'm not arguing for you to have 187 dependencies. I'm arguing that there are times when abstractions are fabulous, but generally they're provided by the standard library or in "blessed" packages that the entire community uses. Here's why relying on standard abstractions over custom ones is likely your best bet:

  1. Abstractions provided by a third party probably didn't predict your exact needs. This is actually for the best, because it means you're abstracting general computing concerns orthogonal to your domain logic, which are much more likely to be properly reusable. list.map and the serde crate are great abstractions. Your UserPolicyManagerI class is not. The difference is we don't actually want locality of behavior, we want locality of pertinent behavior.
  2. Abstractions provided by a third party have almost certainly been used a lot more than your custom ones, which means we have a good idea of how they break and why. We tend not to debug the standard library or "blessed" packages' source code, because it's simply more likely that we broke it, not them.

I currently write Rust full time, and in the Rust world, abstractions are king. There is a temptation when writing Rust, almost like a mirage in the desert, to find the perfect trait or the perfect macro. It's out there, if I just think a little harder, tweak a little further, I'll never have to write this same logic again.

Unless your name is David Tolnay, I promise you it's a fucking lie. Don't do it. You are not special. Write a second function or data type, with a different name, and move on with your life.

But I get it. Abstinence isn't cool, so the best I can do is try to give you advice to help you practice safe abstraction (although I firmly believe abstraction abstinence is a sound policy for most of us most of the time).

If you're going to write your own abstraction, try and keep your domain completely out of it. The goal is to reduce the noise of programming but keep your domain logic together in one place where it belongs. A good rule of thumb is if it would make a good standalone package or extension to the standard library, it's probably a good candidate for separation and abstraction.

"But I don't want to have a bunch of separate libraries I have to update all the time."
If you think you'll need to update it very often, it's not a good abstraction.

Recently (starting around January of 2023), I've been writing a lot of Gleam (oh yeah, this is a functional programming article btw). Gleam is a lovely little language. It's statically typed, and has a very small language surface. There are no macros, and no interfaces.

Every now and then, you have to write boilerplate. And you know what? It's fine. It's more than fine. It's absurdly productive. It turns out, as a prolific blog post by one of the languages core team members explains, all you need is data and functions. Writing, testing and debugging Gleam is usually really straightforward because it's all just functions. In my admittedly limited experience (26 year old mid level engineer), the unintentional complexity in our code bases seems to come from the fancier language features. I get angry at traits and macros a lot more often than I do at simple functions and objects.

If you spend a lot of time in ecosystems heavy on abstraction (Maybe everything is an interface/trait object of some kind? Maybe your language is heavy on macros or "internal DSLs"), I would encourage you to try a really simple language like Gleam. You may find yourself missing your hip extra fancy language constructs at first, but I'd bet you look up after awhile and realize you've gotten a lot done.

Subscribe to BenIsOnTheInternet

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe