Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

While I agree with the statements made in the original post, I'm afraid this thinking can be used as an excuse for avoiding any attempts at finding proper abstractions. Similarly to how the term "premature optimisation" is so frequently used by people unable to write efficient code to excuse for their lack of skill or laziness, despite the context and the times when those words were first used were vastly different and the author meant something else.

IMHO abstraction should not be guided by the desire to remove duplication. Duplication is not even the only (and far from the worst) result of insufficient abstraction.

Insufficient abstraction leads to increased complexity, not just duplication.

Example: just this week I've been working on some code that has to deal with arbitrary ranges of ordered values. Typically when you think of a range, you think of a pair of bounds - the lower and the upper bound. However, the input is allowed to have only half-ranges so that one of the ends might be unbounded. So in the code I inherited there are 3 cases: a range with both lower and upper bounds defined, a range with only a lower bound, and a range with only an upper bound. All code processing those ranges has to deal with that optionality of either end, thus making it way more complex than needed - lot of if ladders or switch statements. And it multiplies very quickly when you deal with more than one range at a time. It is insufficiently abstract, even though it doesn't have any obvious duplication. The proper abstraction would be to transform the half-ranges to full ranges by introducing special open-end items (always smaller or greater than every possible value) which would allow one simple type of range to cover all possible cases.



Very good example of how even a small scale piece of logic can have very messy effects down the line.

Both insufficient and wrong abstractions are viral. They infect everything they touch, which can snowball into large parts being more complex, harder to understand and debug and often also slower.

The wrong abstraction is wrong, insufficient abstraction is wrong.

Really the only weapons against complexity we have as programmers are decomposition and abstraction. We have to take things apart, like in your example it would be the meaning of each parameter, and then we put them together in such away that the details below our abstraction can mostly be ignored.

I say that all with a caveat: I tend to prefer less, insufficient or no abstraction over the wrong one. The former few options can lead to code that is hard to understand as a whole and can be brittle, but the latter drives you into a corner: The only way out is either trying to patch over it or starting from scratch - choose your poison...


> the latter drives you into a corner: The only way out is either trying to patch over it or starting from scratch

Often enough, the way out is going back. Why are developers collectively so reluctant to go back? (Myself included.)


Going back not only means starting from scratch but also adopting anything that the code to be changed touches. It's often a lot of work with uncertainty and thanklessness attached to it.


It's not any worse than introducing a new abstraction when there previously wasn't one. At least in a statically typed language one will quickly identify those touch points. When introducing a new abstraction there is no such help.


Not starting from scratch. But developers never do things like reduplication, or merging modules.

Those concepts are so alien that I believe if I say those words on a recent conversation, lots of people will pop trying insisting on redefining them into meaningless ones.


I'd say this can serve as an example where triplication is better than your abstraction. What are these special open-ended items? How do you need to extend comparison to account for them? Etc. Whereas the three cases are perfectly clear and easy to understand.


The three cases force every code using ranges to deal with them. Apply this way if thinking many times for multiple concepts and you end up with a spaghetti of multiply nested if statements that's near impossible to analyze for correctness. Because now you have to read all code instead of just a tiny subset.

> What are these special open-ended items? How do you need to extend comparison to account for them?

The whole point of abstraction is to make those decisions once and isolate the complexity in one place instead of having it spread over N places in the code, forcing everybody to solve the same problems again and again.


Do ranges not support a limited set of operations through which the rest of the code can interact with them, instead of manipulating the endpoints directly?

I would think of the range itself as the abstraction, and then it matters less how it's implemented since any potential problems are local to the implementation and cheap-ish to fix.


Yes, this is correct. The range is the abstraction, and then you can choose how to represent it. Not much difference between the three range cases, and the single case with special endpoints, except that the three cases are more general, as no special values are needed.


Sure you can probably also do it, but this is not the way how the code was originally written. The original ranges present the bounds in their public API, and most code just operated on them.


Then I would say that's the problem. Whatever implementation you leak, the problem is the lack of implementation hiding, not that the implementation looks this way or that way.


But that's still insufficient abstraction and my general point holds.


I agree, your point still holds and if anything becomes stronger, because now we've identified a concept at a higher level of abstraction to replace endpoint operations!


It does not. It's not about abstraction, but encapsulation.


Hard disagree here. Encapsulation is just one aspect of implementing the abstraction. And I'm not even convinced it is the best way here. Leaving the ranges as simple structures with two public fields, but introducing an "infinity" concept is another way to go. No encapsulation but still abstract (although one may argue this is still encapsulation but at a different level - applied to range bounds instead of whole ranges).

Encapsulation of ranges doesn't actually fully solve the problem, but just moves and isolates the complexity to the private implementation of the range concept (likely a class in OOP). E.g. you want to compute if two ranges overlap - you still have to deal with the complexity of 3 cases in each argument, so total 9 cases.

And hiding the bounds is likely going to be a lot more intrusive on the existing caller code (more refactoring).

Or could use both. ;)


>The whole point of abstraction is to make those decisions once and isolate the complexity

The point Sandi Metz is making is that it's often only obvious when you can do this in hindsight.

Lots of things can look abstractions worthy but aren't - including things that are repeated 3 or even 10 times.


When your three cases run into someone else’s two you have six, and it can even get much worse than that. Or an object running into itself and getting nine cases.

Intervals are nice for representing acceptable ranges. Half intervals mean greater/less than. If you stick infinities on the ends, everything likely works. You then expose methods or functions for all your operations. From the outside, you don’t have to care if it’s a half interval or not (unless that is what you’re particularly checking). On the inside you don’t really, either.

If you’re messing with intervals in a business setting, it’s worth considering if you need multi intervals, non continuous regions.

These are all great for handling uncertainty. Like if you add two weights that have +/- values, you can have the sum have those and be correct. The math is all well defined and rather easy. Wikipedia has good pages on it.


> How do you need to extend comparison to account for them?

The post already says: −∞ < x < ∞ for all numerical x. (And the mathematician in me clarifies that that's all real numerical x.)


I don't remember who said that, but mathematics is all about building abstractions, not about computation. So many times mathematics helped me make code simpler.


The post I am replying to made no assumptions about the domain the order is defined on. If it is over the reals, sure, you can use -∞ and ∞. If it is over the integers, you can use MIN_VALUE and MAX_VALUE, sacrificing some of your domain (which might be a problem, depending on the context), or you can use Option[Int], which comes with performance issues.

Or, you can use a range which is a sum of three/four cases, and not worry about any of that.


> What are these special open-ended items?

> If it is over the reals, sure, you can use -∞ and ∞. If it is over the integers, you can use MIN_VALUE and MAX_VALUE

If you already knew the answer, why did you ask?


The time dimension is often forgotten when applying these maxims. When we see code, we often fail to consider the the journey it's taken to arrive at that point in time, and where it might be headed in future.

In the example you set, it's the right time to apply an abstraction, so it's no longer premature. Perhaps the maxim should be labelled as "premature abstraction", rather than "premature optimisation".


> The proper abstraction would be to transform the half-ranges to full ranges by introducing special open-end items (always smaller or greater than every possible value) which would allow one simple type of range to cover all possible cases.

You wouldn't even need to create anything new—both math and C already provide this abstraction in the form of −∞/-inf and ∞/inf.


I wasnt talking specifically about real (float) numbers, but yes - this is that abstraction. And it generalizes to any type with ordering (can work with integral types as well).


I'm encountering a lot of these types of small abstraction projects in a React project I'm working on. It's a music theory "explorer" app and, maybe unsurprisingly if you know any music theory, getting a good abstraction that doesn't fall victim to lots of weird little edge cases is tricky.

I'm using Tonal which makes it easier, because I can mostly push weirdness into wrappers for individual Tonal calls. It's honestly been a great little challenge because the scope is so small that it doesn't take all that much analysis or thought to see where abstractions break down. Fun little exercise in code design.


IDK anything about music theory but I wonder, if you're having trouble finding a good at abstraction to express theory, perhaps the theory is at fault.

I mean, at a high level, theory is the abstraction isn't it?


It's really use case specific in my case - things like having a selected key should be straightforward enough, but some of my components weren't written with that in mind. It's little things like that that make me say it's a good exercise - many of the difficulties are my own fault, which means it's easy to learn from my mistakes.

Music theory in general is a somewhat difficult abstraction due to the multiple ways to interpret different things in different contexts. The same chord progression might be thought of as being in several different keys based on other contextual information for example.


Problems like these often come from the pressure to ship fast, and not writing code 2-3 times to find a good way to express something. If you're going to rush through abstracting things away, I'd rather you duplicated. If you will take time to express it well, then I'd prefer a good abstraction.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

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

Search: