So glad to see the Law of Leaky Abstractions in there - that's had a very long-running impact on how I think about programming. It's still super-relevant today, nearly 18 years after it was published.
It's nonsense, written because Joel had never used a language with a decent type system. Any Haskell programmer uses half a dozen non-leaking abstractions before breakfast. Even the examples in the post itself don't hold up - using UDP instead of TCP doesn't actually mean your program will work any better when someone unplugs the network cable.
I think we Haskellers have to be realistic and say that although it feels that many of our abstractions are non-leaking, they're only non-leaking in the sense that a modern, triply-glazed, thoroughly insulated house is non-leaking of heat compared to a draughty, cold house built 200 years ago. There are indeed leaks, but they are small and generally ignorable.
I don't think that's true. A lot of these abstractions are provably correct and so simply cannot leak (and in slightly more advanced languages you might even enforce those proofs - consider Idris' VerifiedMonad and friends).
Of course if you put garbage in at the lower levels (e.g. define a monoid instance that doesn't actually commute) then you will get garbage out at the higher levels (e.g. the sum of the concatenation of two lists may no longer equal the two lists' sums added together), but that's not the abstraction leaking, that's just an error in your code.
You are abstracting over a CPU and memory. Your abstraction leaks in that memory layout actually matters for performance, for example. Or if you have a bad RAM chip.
> Your abstraction leaks in that memory layout actually matters for performance, for example.
There are cache-aware abstractions if your situation warrants them. Of course if you abstract over a detail then you lose control over that detail. But that's not the same as a leak, and it's the very essence of programming at all; if the program needs to behave differently every time it runs, then creating a useful program is impossible.
> Or if you have a bad RAM chip.
That's another example of what I said about garbage in, garbage out. The fault isn't in the abstraction, the fault is the bad RAM chip. If you were manually managing all your memory addresses then a bad RAM chip would still present the same problem.
That's not exactly false, but at that point you might as well say that anything that breaks is an abstraction leak. If my car won't start in the morning, is that an "abstraction leak"? I don't think it is (or at least I don't think it's a useful perspective to see it as one), because the problem wasn't that I was thinking of the abstract notion of a car rather than the details of a bunch of different components connected together in particular ways; the problem is that one or more of those components is broken (or maybe that some of the components are put together wrong).
> You are abstracting over a CPU and memory. Your abstraction leaks in that memory layout actually matters for performance, for example.
I find the idea that an abstraction is leaky if different implementations of it perform differently to be fairly useless. I don't think it's a useful concept unless the abstraction captures the expected performance. If the abstraction doesn't give any performance guarantees, then the caller shouldn't have any performance expectations.
Similarly to abstractions around accessing a file on disk that might fail. The abstraction should account for potential failures. If it doesn't account for failures, but the implementation does fail, then it's meaningful to call it leaky.
Performance matters sometimes. If you're in a situation where performance matters, and the abstraction doesn't capture the expected performance, but you're subject to the abstraction's actual performance anyway, then the abstraction leaked in a way that matters to you.
You've found that the abstraction isn't useful for doing your task. That's not leaking. That's like complaining that your ice cream maker can't cook rice. That's not what it's for.
In fact, this is a common manner for abstractions to become leaky. You find you are in need some guarantee not present in the abstraction. You choose to add whether or not that guarantee is satisfied to the shared interface. Congratulations! You've added a leak to the abstraction.
But that's not the only option available. If you need a guarantee not provided by an abstraction, you could ignore the abstraction and use something that actually provides the guarantees you need.
Abstractions are equivalences, not equalities. You shouldn't expect an abstraction to make a linked list the same thing as a vector - they aren't, and they never will be - but they are equivalent for certain purposes, and a good abstraction can capture that equivalence. The performance of those two different collections is not the same, but that's not a leak unless the abstraction tried to claim that it somehow would be the same.
> You shouldn't expect an abstraction to make a linked list the same thing as a vector - they aren't, and they never will be
I would even argue that's the point of an abstraction. Hide the details that don't matter to the caller. If performance is a detail that matters, and the abstraction doesn't capture it, then you're using the wrong abstraction.
Yeah, and that is still completely pointless observation, because literally nothing I do will change because of it. Because if abstraction leaks in these edge cases, we are still better off trying to come up with same abstractions then not.
The theories as conceived to relate to the actual programming environment. Any proof about a Haskell program’s correctness relies on a leaky abstraction (an axiomatization) of what will actually happen when you run GHC on the source file.
WTF? The set of integers isn't finite. There are non-leaky ways to represent integers or computable reals in a computer (of course one cannot compute uncomputable reals, by definition). And plenty of finite subsets of either are well-behaved and non-leaky. If you treat a finite subset of the integers as being the set of all integers then of course you will make mistakes, but that's not a problem of abstraction.
Is it possible that you are misinterpreting what Spolsky meant? I think he means that in the real world we interact with implementations of abstractions, and that the implementation always shines through and can bite you in the ass. This is what makes side-channel attacks possible, and (in Spolsky's view) unavoidable.
> I think he means that in the real world we interact with implementations of abstractions, and that the implementation always shines through and can bite you in the ass.
I understood fine. He asserts that "always" on the basis of a handful of examples, only one of which even attempts to show anything more than a performance difference. It's nonsense.
I think you're giving Haskell too much credit... In my experience most abstractions need to be replaced because of performance requirements - achieving lower latency, higher throughput, etc. That's the reason to go with UDP instead of TCP. Not sure if this sort of leakiness falls under what Joel had in mind though.
IMHO the switch to UDP is happening because the work TCP is doing to ensure reliability is now done at network and thus having TCP do it is redundant. TCP assumed very simple and dumb network, which is no longer the case.
More or less reliability in the datagram layers affects performance - for example, WiFi does its own retransmissions whereas ethernet does not, because WiFi uses a less reliable physical layer, and because you don’t want your packets to have to go from London to New York and back before you discover one of them was lost.
But reliability at the WiFi later cannot give your application the semantics of an ordered data stream, so it is not a substitute for TCP. You can replace TCP with a different transport protocol if you want different behaviour, eg SCTP or DTLS or QUIC, but in all cases they are providing a higher level abstraction than raw datagrams, not just (and not necessarily) more reliability.
1. Every type has a bottom in every mainstream language, most of them are just less explicit about it.
2. Bottoms do not make abstractions leaky in some generalised sense. The "fast and loose reasoning is morally correct" result applies: any abstraction that would be valid in a language without bottoms is still valid wherever it evaluates to a non-bottom value.
I agree. Dijkstra et al. always pushed (as early as in the 1960s) that resources used at abstraction level n should be effectively invisible at level n + 1. Anything else is an improperly designed abstraction.
Of course there's always the thermodynamic argument that "any subprogram has the permanent and externally-detectable side effect of increasing entropy in the universe by converting electricity to heat" but that is to me a bit of a Turing tar-pit of an argument.
Even that's just an effect that you can represent in your language. The evaluation of 2 + 2 is not exactly the same thing as the value 4, but you could track the overhead of evaluation (e.g. in the type) and have your language polymorphically propagate that information.
My takeaway from it is that we need to distinguish what you might call essential from incidental duplication. Essential duplication is when two bits of code are the same because they fundamentally have to be, and always will be, whereas incidental duplication is when they happen to be the same at the moment, but there's no reason for them to stay that way.
For example, calculating the total price for a shopping basket has to be the same whether it's done on the cart page or the order confirmation page [1], so don't duplicate that logic. Whereas applying a sales tax and applying a discount might work the same way now, but won't once you start selling zero-rated items, offering bulk discounts to which a coupon discount don't apply etc.
[1] Although i once built a system where this was not the case! In theory, the difference would always be that some additional discounts might be applied on the confirmation page. In theory ...
I don’t understand why people interpret that article as recommending you avoid prematurely removing duplication and comparing it to the rule of 3. The point of her essay is that you should resist the sunk cost fallacy and refactor your duplication-removing abstractions when requirements (or your understanding of them) change.
The problem isn't DRY. The problem is most programmers' inability to tear down abstractions that aren't correct for your new requirements when they evolve.
Yeah that sucks. Especially when you're designing service endpoints and your colleague insists upon reusing an existing endpoint instead of opening up a new one, because the two use cases looked the same when he squinted hard enough.
Now instead of /credit-card and /debit-card, which are independently testable, debuggable and changeable, you just have /card. Can't change the debit logic in /card because it will break credit. Can't change the credit logic in /card because it will break debit.
Well... early in my career, a colleague and I developed a bit of a mantra: "Did you fix it everywhere?" Later in my career, I learned the value of there only being one place to have to fix.
At the same time, too much DRY can over-complicate (and even obfuscate) your code. That's not the answer either.
Taste. Taste, experience, and wisdom. But I don't know how to give them to someone who doesn't have them. Maybe by pointing out the problems of specific things they're trying to do, in a way that they (hopefully) can understand and see why it's going to be a problem. Maybe...
by everything you mean outside of programming also? Or just in programming? I guess I find DRY pretty important for me, as a tool to force me to abstract and to help me understand the system I'm working on.
https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-a...