In Defense of Leaky Abstractions

They can be a good thing

by Marcel Garus · 2024-10-10
available at www.marcelgarus.dev/leaky-abstractions

In programming, a leaky abstraction refers to an abstraction that leaks implementation details that it is supposed to abstract away. Joel Spolsky' Law of Leaky Abstractions theorizes the following:

All non-trivial abstractions, to some degree, are leaky.

For example, TCP protocol attempts to abstract unreliable networks by retransmitting messages. This doesn't work all the time. If the network is unreliable, TCP operations can have wildly varying performance or may never complete.

Instead of trying to fix leaky abstractions, I've recently come to like the technique of embracing them and building guardrails around them. In fact, I believe there are cases where intentionally adding more leakage than strictly necessary can be beneficial.

Rust's unsafe

While Rust is generally a memory-safe language, sometimes you need to sidestep the compiler's memory-safety checks to implement new data structures or use external functions. Rust has an unsafe keyword for that.

fn foo() {
    unsafe {
        // You can dereference pointers here
 ...
 }
}

Even though unsafe increases the complexity of the Rust language, it brings a huge benefit: Efficient data structures can be implemented directly in Rust.

Martinaise's Assembly Functions

In Martinaise, you can write functions in a custom assembly instead of the language itself:

opaque Int bytes bigbytes aligned

fun +(leftIntrightInt): Int asm {
 moveib a 8  add a sp load a a | left
 moveib b 16 add b sp load b b | right
 load c sp | return value address
 add a b store c a ret
}
fun -(leftIntrightInt): Int asm {
 moveib a 8  add a sp load a a | left
 moveib b 16 add b sp load b b | right
 load c sp | return value address
 sub a b store c a ret
}

It's a custom assembly variant where each instruction gets compiled into a single byte code instruction. Interfacing with assembly works seamlessly because types have a deterministic memory layout and Martinaise has a pre-defined calling convention.

Even though Martinaise explicitly leaks the underlying byte code compilation target, the language itself gets simpler: Instead of the compiler magically implementing many types like Int or Float, the only "magic" comes in the form of the assembly functions.

A cozy language feeling

Leaky abstractions give me a cozy feeling. When writing assembly functions or unsafe code, it kind of feels like working in the engine room of a boat: You get to peek behind the curtain of the usual language and work on a lower level. When diving into the implementation of code, you never hit a wall of compiler magic – you just get dropped one level lower.

a cross-section of a boat with an engine room