Forget about Leaky Abstractions

In 2002, Joel Spolsky defined the Law of Leaky Abstractions.

Tweet ThisAll non-trivial abstractions, to some degree, are leaky. –Joel Spolsky

His primary example for a leaky abstraction is TCP. It provides the abstraction of reliable communication. Unfortunately, the layer below, IP, is unreliable. To fix that TCP uses confirmation messages and resends missing packets, which leads to slowness or in Spolsky's words "it leaks".

Another example from Spolsky are uniform memory accesses, which leak in the sense that caches lead to non-uniform performance. Likewise, SQL statements have different performance characteristics even if they look equivalent. Network file systems leak the fact that networks are not as reliable as accesses to a local disc. The C++ string class cannot hide the fact that string literals are char pointers.

Spolsky sees this leakyness as a problem for software development in general. If abstractions were perfect, we could use them, forget about the layers below, and enjoy the simplicity. Since abstractions leak, we must also learn the lower layers. As we build a stack of layers the amount of necessary knowledge grows, although the necessary knowledge for the top layer is ever smaller. The alternative is to accept the increasingly worse performance.

Smart people identified the problem before. For example, Gregor Kiczales describes it in Towards a New Model of Abstraction in Software Engineering (1992):

The "abstractions" we manipulate are not, in point of fact, abstract. They are backed by real pieces of code, running on real machines, consuming real energy, and taking up real space. [...] What is possible is to temporarily set aside concern for some (or even all) of the laws of physics.

Note the "temporarily", which refers to the leakyness. The solution Kiczales proposes is the Metaobject Protocol (MOP), which enriches abstractions with additional means to influence the implementation. He also quotes Wirth from On the Design of Programming Languages (1974):

I found a large number of programs perform poorly because the language's tendency to hide "what is going on" with the misguided intention of "not bothering the programmer with details".

Wirth was talking about designing programming languages and compilers, but it generalizes well to all kinds of interfaces.

Leakyness depends on the User

In three cases (TCP, Uniform Memory, and SQL) the leak reveals itself as a performance problem. This means as long as performance is not an issue the abstractions are perfectly fine. There are reasons (usually backwards compatibility) why imperfect abstractions are not fixed.

Even if an abstraction is leaky it can still be useful. Sometimes you cannot escape it (uniform memory) and sometimes the workaround is costly to implement (TCP, SQL). So you accept the technical debt for now. Hope the debt does not kill the project. Maybe there will come a time where it is worthwhile to pay off the debt.

In the mean time there is no point in sulking in the leakyness of your abstractions. Instead, let us consider a more useful classification for imperfect abstractions.

Incomplete Abstractions

Guillermo Schwarz prefers to call abstractions "incomplete" or "wrong". If an abstraction requires the user to consider the lower layers as well, it is incomplete and should be extended. From the examples above network file systems are incomplete. If the normal file system interface would be extended with timeouts the abstraction would be closer to perfect. Likewise memory accesses could be extended for more explicit cache handling and C++ compilers could wrap string literals into string classes.

I do not like the word "wrong", because it suggests that the abstraction is broken. However, an abstraction might be fine for others. I prefer "unsuitable", because that is more neutral.

Tweet ThisIf an abstraction is not perfect for you, it is incomplete or unsuitable

For SQL it is unclear if the abstraction is incomplete. If database developers can fix the performance imbalances the abstraction was incomplete. If it is impossible, the abstraction is unsuitable. You can also find the same situation one layer higher if you use an object relational mapper (ORM). Sooner or later you hit cases where you would use joins on the relational layer, but on the object layer there is no equivalent. Any workaround hurts performance because it cannot handle it within the database and must transfer data around.

Incomplete or unsuitable? It depends. It could be an unsuitable abstraction, if a NoSQL or eventually-consistent database is good enough for you. It could be incomplete, so you use vendor-specific enhancements like stored procedures, for example.

Another good example is garbage collection. It hides the manual memory management, but as many Java developers know getting peak performance out of the garbage collector is hard work. One possible workaround is to reuse instead of freeing and reallocating objects. This builds another abstraction on top of garbage collection which garbage collection actively hides. You still have to pay the cost of garbage collection, larger memory consumption.

Incomplete or unsuitable? It depends. It could be an unsuitable abstraction and manual garbage collection is the key. It could be incomplete and you just need to find the right garbage collector (maybe Azul C4?) and tune it just right.

A third example are integers. We are so used to basic data types, we rarely question them. Andrei Alexandrescu's keynote at DConf 2017 was about his experimental checkedint abstraction, which is a wrapper for integers and allows to enhance the operations with user-defined policies. For example, you can throw an exception at overflow, forbid implicit casting at compile time, check that integers stay within bounds, or compare signed with unsigned using proper checking. The fact that you might want to adapt all those things tells me that the abstraction int is incomplete. Thanks to D's meta programming Alexandrescu can fix this with minimal overhead. His solution is related to Kiczales MOP from above. In his paper Kiczales says: "for those cases where the underlying implementation is not adequate, the client has a more reasonable recourse. The meta-level interface provides them with the control they need to step in and customize the implementation to better suit their needs."

To summarize: The fact that abstractions are leaky is for whiners and not useful. Forget it. Instead, find out if your abstraction is incomplete or unsuitable. Then you can extend or switch it.

I used parts of the article for my my TopConf talk "Abstractions: From C to D".

If an abstraction is not perfect for you, it is incomplete or unsuitable