The Law of Leaky Abstractions (2002)
joelonsoftware.com
joelonsoftware.com
For example, after discussing the subject of promises (in the sense of the tool to manage and abstract asynchronous program flow) with a colleague, it occurred to me that the reason we care about async behaviour is that a "function call" is a leaky abstraction, in that it takes a variable amount of time.
We think of a function call as "doing X, Y and Z". The fact that it takes time to do it, blocking our 'thread of execution' causes us the problem that we can solve with the overhead of async programming (or threading, or...)
(By 'function call' I'm glossing over system call, library call, embedded cpu-eating loop.)
From this perspective, you can see that an alternative to working around the leak is plugging (some of) the leaks - which is the approach golang takes, by shuffling around threads of execution to avoid them being stalled by blocking system calls.
That perspective is useful to me and helps me understand why I think I prefer the golang approach, "more robust abstraction".
My only complaint is that once you find some really good writing in this field, you realize how hard it is to come by (in print, anyway). I happened to find "Joel on Software" in the "Professional Computing" section at Barnes & Noble. That and "Coders at Work" are the only books worth reading I've ever seen in decades of browsing that section. Still, I'm glad I came across them.
An abstraction in mathematics is a simple, small idea that we use to describe and understand a large variety of objects. For example the concept of an algebraic group is an abstraction that can be applied to numbers, functions, matrices, etc. We can understand all these objects using the same concept.
Are all words this way? Take the word "orange". Are all oranges the same? They all have variations in taste, texture, color, etc. They are all physically separate objects. But we use the same idea to understand and describe them. Maybe this is more clearly seen with the word "fruit".
This applies to verbs too - think of the verb "to run". Usain Bolt runs in a very different way from normal people, but we use the same concept to understand what they do.
These abstractions "leak", i.e. cause confusion when the underlying reality doesn't match up. Is a tomato a fruit? If I trained as an Olympic sprinter would I have to abandon misconceptions about how to move my body, misconceptions caused by a lifetime of using the word "run"?
Is every word an abstraction?
I've seen a single-platform JVM bug. once. The only definite OS failure I've ever seen was bad RAM, and you could write a bootable program that ran without an OS and that wouldn't protect you from bad RAM. I've seen one CPU flaw, and linux abstracted over it seamlessly. I've never seen a failure that was the result of analogue chip behaviour. There are plenty of non-leaky abstractions around.
For example if your SQL query is too slow from a pragmatic point of view and then you go ahead and fix it by using optimizer hints then the abstraction of the procedural querying process provided by SQL has just failed you as it hasn't worked.
I agree that some abstractions are very good, but that does not make Joel's point moot. The key thing is to understand that abstractions are always some sort of tradeoff between the virtues of directness and the virtues of productivity.
Btw: I am really surprised that this essay had never been posted to HN before.
Well sure. But I've never had that happen; is the A = B and B = C and C = A problem the article talks about one that causes problems in real-life code?
(I'm not picking on Postgres, it's a wonderful DB, but you need to understand how your tools are working.)
I don't know what your particular area of expertise is, but in my experience, performance is the reason that most abstractions fail, because there are a lot of cases where I can't simply accept the poor performance provided by the abstraction. All of a sudden I have to start worrying about what is going on behind that interface so that I can use the interface in an optimal way. And once you have to start worrying about what is behind the interface, you're in leaky interface territory.
Here's my favourite example - allocators. Is my memory manager smart enough to detect that I'm allocating and disallocating a bunch of buffers of the same size, and will decide to keep some in cache for me? Can it see that I am doing a lot of allocs and frees from one thread, so it uses a special non-thread allocator for these allocs? These can be big wins in performance, and I can't leave them on the table.
Some allocators do these things, but there is generally no way of knowing just by looking at the API. So you have to start hoping that the documentation explains what is being done behind the scenes, or that you have the source code, or be prepared to do some performance tests.
Here's another example where an API works hard to hide a performance leak - OpenGL. Internally most OpenGL ES implementations hold texture buffers in a tiled format. This allows the GPU to copy the texture to the screen at a similar speed, no matter the orientation of the texture. If this wasn't done, you would get massive differences in performance if you rotate a texture by 90 degrees, due to memory paging issues. But with tiling this is not the case, performance is nearly constant. But this type of sensitivity to performance-based abstraction leakage is rare (and indeed creates another leak - you now need to be aware that there are potentially two copies of your texture in the system - the one you write into and the one that is actually being used by the GPU).
In my opinion it is nearly impossible to create an abstraction that doesn't leak implementation information. This is not to say that abstractions are worthless, but just to highlight that abstractions need to anticipate this and provide the tools to make dealing with the consequences a bit easier.
however, i do agree with your point about performance being a common leak - the most recent MS STL is an interesting example. its performance is quite horrific in some circumstances due to some very sledgehammer thread-safety approaches which are not necessary but protect bad programmers from themselves (a recurring theme in many such libraries) - in this case the abstraction is leaky in that I discover implementation details by profiling things that wouldn't normally be expected to be a performance problem... just like you say.
I think most of his abstractions are not leaky, but they come at the cost of poor performance in certain cases.
In any case, his point is correct: it's hard to use complex systems to implement a simple interface, because it's hard to create the glue code so that the simple interface drives the implementation in the "right" way in every circumstance.
There is, of course, another approach that the author didn't mention, which is providing a mechanism in the interface for "hints", e.g. "please store this matrix in column-major format". That is not a perfect solution, but it is another tool for better abstractions.
> All non-trivial abstractions, to some degree, are leaky.
It itself is somewhat leaky: it never defines "non-trivial"! Perhaps "non-trivial" just means "complex enough to be leaky", rendering the whole thing a tautology.I've been thinking about different philosophies behind abstraction for a while. My ideas are a little unformed, but I think they're still useful. Basically, as I see it, there are two different ways to define abstractions.
There's the usual way: you come up with a new notion that has additional capabilities. The abstraction adds to the underlying concept. Perhaps it also hides some detail, but the main action is adding. Some of the leaky abstractions from the post really fit this: TCP, for example, adds new capabilities and behavior over the underlying protocol; C++ strings add a bunch of string-specific capabilities over char arrays; NFS adds filesystem capabilities to a network connection.
These abstractions are largely "leaky" because what they add does not always map nicely to what's underneath. They're also interesting because each abstraction only fits one thing--TCP is just for network communication, C++ strings are just text in char arrays and so on. Ultimately, all these abstractions are just more structure over whatever they're based on. Again, the key idea is adding.
The other sort of abstraction is what I think of as "algebraic" or "Haskelly". Instead of adding, these abstractions take away. They expose some feature of the underlying concept and throw everything else away. A monoid, for example, just exposes an associative operation from whatever set (or type) it's defined over. It can't really be "leaky" (at least in the same way) because it's showing what's already there. The normal sort of abstraction is like a structure built on top of a foundation; this sort of abstraction is like a lens or a view into a property of its foundation.
A very important result is that these abstractions are very independent of any basis or use-case. Monoids come up everywhere. Strings form a monoid. Numbers form a monoid (in many different ways, even!). Booleans form a monoid. Certain classes of functions form monoids. Note how I said "form": even if we ignored it, the monoidal structure would still be there. Talking about these things as monoids doesn't add anything but just highlights something already present.
Instead of adding capabilities to the underlying object, this sort of abstraction instead exposes structure shared by otherwise distinct objects. Monoids represent things you can combine; groups represent things that are somehow symmetric; rings represent things that are sort of like numbers; fields represent things that are really quite a bit like numbers; F-algebras represent things that are like any of the other abstractions I've listed (meta-abstraction!); functors represent things that can be transformed while preserving structure and so on.
These abstractions also let us talk about different things uniformly and generically. We can reason about and write code against these abstractions without worrying about any particular underlying object. By doing this, nothing but the abstracted structure can do anything--details of any given thing can't leak through because we have to be generically compatible with every instance of the abstraction.
Functors are a great example of this. If we wrote code using lists, we could do all sorts of things: always return an empty list, add elements to the end, drop elements from the front, change the order... If we wrote a code just using a functor, it would still work for lists but all we'd be able to do is map a function over the elements. Nothing else. Always returning an empty lists wouldn't even be a meaningful construct! Clearly, using this abstraction has taken away our capabilities, but it turns out this is often a good thing.
But this is where "non-trivial" comes to bite us. Are any of the abstractions I listed actually "non-trivial"? I'm not sure I could claim that. To be generally applicable, all of them have as little structure as possible. Ideally, you want to limit your capabilities as much as possible to express whatever you care about. This usually means limiting them a lot. All of the things I listed can be expressed as a handful of functions and a handful of algebraic laws--a few lines of description is enough to understand them completely. They are nowhere close to something like TCP!
Ultimately, I am not even sure it really makes sense to compare the two sorts of abstractions. But they are certainly both approaches to abstracting, albeit in different ways. It's something that's worth thinking about the next time you have to design or use an abstraction yourself.
So you're saying that this particular abstraction of abstractions is leaky? ;)
That might seem to not be a huge deal, but mathematicians are aware and programmers should become increasingly aware that good choices of constraining laws allow for both massive expressivity while also having enormous capability for anticipation and introspection. My favorite example is the preponderance of groups showing their large variety coupled with the intense, sophisticated structure unveiled during the Moonshine project which attempted to categorize all the kinds of groups that can be derived just from their core, law-fixed properties.
That's why the monad laws are so important. They extend the tiny, simple interface into a robust method of reasoning that cannot leak in so far as the precise promises it makes... The promises derivable from its laws.
(Unless you don't have a "real" monad)
a software engineering abstraction which doesn't have clear performance facet is just a bad software engineering abstraction (while, for example, it may be a really good math abstraction).
It is entirely possible to produce an abstraction over some concept that is capable of expressing the exact same things but brings different areas to the forefront. I think assembly language acts as a simple example of this. You can express all of the same things you can with machine code but it is clearly an abstraction.
Edit: grammar
Text.unpack (s1 <> s2) === Text.unpack s1 <> Text.unpack s2
which bring to the forefront that both the Unicode safe Text type and the String type share that monoidal structure.Perhaps "generalization" is a good term for abstractions that removes things. I like to think of Haskell's many features as generalizations.
http://ttic.uchicago.edu/~dreyer/course/papers/wadler.pdf
Every programmer or computer scientist should read that paper. Wadler is clear enough that even without knowing typed lambda calculus, you can understand his point.
PowerPoint came out in 1990 - what's the PowerPoint of 2020?