Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
The Law of Leaky Abstractions (2002) (joelonsoftware.com)
87 points by beltsazar on Dec 18, 2013 | hide | past | favorite | 31 comments


I continually find this particular point useful and enlightening. (Joel's whole corpus of stuff is worth a read. At the risk of causing a hacker news "joel storm" of submissions, check out the pricing/segmentation piece: http://www.joelonsoftware.com/articles/CamelsandRubberDuckie...)

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".


Agreed about Joel. When I first read his "corpus", it was already a few years old. But whenever you get to it, get to it.

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.


Leaky abstractions are precisely why it's important to understand what's going on at least one level below where you're working at. This has always been true of computers, whether you're working at a script level, system language level, assembly, or even hardware. Eventually something will break the abstraction, and on that day you'll be there looking at that stack trace or register + memory dump or burnt out nand gate or cold soldered capacitor or faulty wall circuit, and not knowing what to do about it.


The constant churn of web technologies make this really frustrating, because you have an entire ecosystem of people that doesn't know what their tools do. "Just run re-war again, dude, that will fix it."


This is a concept that I've discussed frequently with team members. Related: if you abstract away the differences between A and B, you likely lose some of their unique advantages. A and B could be databases, hard drives, etc. It may be worth it, but it's a tradeoff.


Can every word be considered an abstraction?

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?


The examples don't support the argument. It's not like writing your application using UDP and expecting some packet loss is going to mean that your application continues to work when someone unplugs the network cable. And the other examples aren't really leaks, just performance disadvantages. I might accept an argument that any sufficiently powerful abstraction imposes a performance penalty, but that's a lot less interesting.

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.


"And the other examples aren't really leaks, just performance disadvantages."

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.


I think saying that some examples are just performance disadvantages rather than leaks misses the point. "Leaky" is a metaphor here, so assuming that any kind of actual data loss is necessary for it to be the case seems incorrect to me.

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.


> 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.

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've had to rewrite lots of SQL queries (for Postgres) that an intelligent database should have just handled for me. I'd prematurely exclude certain columns that it could be mathematically proven from the query I would never need and see amazing speed-ups.

(I'm not picking on Postgres, it's a wonderful DB, but you need to understand how your tools are working.)


i've seen a few one off hardware fails from presumably external factors like neutrons slamming into things... i've found a small handful of compiler bugs, although nowadays this is considerably rarer than it used to be (i haven't seen one in about two years and the last one was in a proprietary 'obviously not quite finished' compiler), and from working with rendering and OpenGL a bewildering number of driver bugs - some deeply embarassing like using GL_CW to signify counter-clockwise vertex winding. these cases aren't really leaky abstractions though - just bugs and i don't need to know the details to find them. this is just buggy abstraction imo.

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.


This is besides the point, but I would just like to point out that Linux does have a kernel argument called "memmap" which can sometimes allow a system to operate normally by excluding a range of bad RAM addresses from being used.


A true leaky abstraction is where the behavior of the interface is forced to reflect some of the complexity of the implementation, but in a way that is surprising given the rest of the interface.

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.


Here's the actual law:

    > 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.


I think another vital part of describing the Haskelly abstraction type is the existence of laws. It's not just that an object fits an interface but further that some non-trivial amount of its behavior is specified by the laws that constrain it.

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)


I'm inclined to agree that this law is something of a tautology. If nothing else, I think it is the wrong way to look at abstractions. I think it was Rich Hickey who said something to the effect of "abstractions are not about hiding, they are about pulling something to the forefront".

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


In my own experience these "Haskelly" abstractions do a fantastic job "bringing something to the forefront". I love when I'm working with a parser and switch from Monadic mode to Applicative mode as it emphasizes a notion of application sequencing instead of procedural sequencing. When I concatenate strings using (<>) instead of (++) I emphasize that strings can be thought of as lists which are free monoids. Especially nice are the laws like

    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.


... and in the end you still have talk over TCP and deal with that leaky abstraction, or you accidentally write O(N^2) code that could be expressed easily in O(N) (compare https://github.com/nominolo/HTTP/commit/b9bd0a08fa09c6403f91... for a recent HN submission) because the "taking away" approach at abstraction also abstracts out some important performance considerations.


TCP is done via the IO monad and this is an excellent example of what the IO monad is used for---it forces you to realize that there's another layer of (this time quite leaky) abstraction over your potentially simple inner abstraction.


That's a bad example because this code does not use the Monoid abstraction, but rather the Buffer Operations abstraction which does expose performance costs.


>because the "taking away" approach at abstraction also abstracts out some important performance considerations.

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).


>> Ultimately, I am not even sure it really makes sense to compare the two sorts of abstractions.

So you're saying that this particular abstraction of abstractions is leaky? ;)


I honestly can't say if you guys haven't read Douglas Hofstadter or if you have read it and love it a bit too much.


Great explanation :)

Perhaps "generalization" is a good term for abstractions that removes things. I like to think of Haskell's many features as generalizations.


All these abstractions leak on actual hardware - a haskell program running on faulty hardware does not necessarily satisfy any proposition you can prove on paper. I think you're also confusing the mathematical concept of abstraction and the programming/CS concept of abstraction.


I used to think all abstractions were leaky until someone explained parametric polymorphism and free theorems to me.

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.


This is why I think Bret Victor's stuff, while incredibly cool and interesting, will have a tough time living up to his vision.


On the other hand, think of how much power a program like PowerPoint gives the average user without forcing them to learn the underlying technologies. Or the iPhone. I think that user interface design solves most of the leakiness problems, and the illusion of seamlessness is critical to good interface design. Remove as many features as you have to so that the abstraction doesn't leak.

PowerPoint came out in 1990 - what's the PowerPoint of 2020?


All good points, however Bret Victor has specifically been showing off alternatives to low level programming by building very high levels of abstraction. He has even apparently said that he thinks the development community is purposely keeping programming complicated to maintain the demand for their specialized skills. I think the law of leaky abstractions is why it is simply not possible to elevate programming to a level of abstraction that allows people without specialized skills to implement arbitrary applications.


(2002)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: