I actually went into this with similar misgivings. A lot of "get rid of ifs" advice, particularly from OOP people ends up making things more complex IMO. However most of the advice other than the "Switch to Polymorphism" seem very down to earth and a simplification. Less about getting rid of ifs as if their mere presence was a stain on humanity and more how to rewrite what they are doing more clearly.
It seems the biggest thing OOP polymorphism has going for it is that it is "open", that is, the possible derived classes need not be known in advance. But, the fact that our toolchains use a separate link step, and that linkers remain primitive and link-time code generation is not widespread, is merely an historical accident. There's no a priori reason things have to be this way.
For example, an alternative would be if all of the source for an executable were in a single image file a la Smalltalk, and compilation consisted of converting this "database of code" into a single executable (no shared libraries!) in one big-bang compilation step (no object files, no static libraries!) You could even have something which syntactically resembled open polymorphism, maybe even using existing languages almost as-is, but instead of compiling down to type erasure and dynamic pointer casts, would compile down to Sum Types (variants). (The compiler could scan the image to find all of the derived types it could be and could construct a variant out of that.)
I'm not saying this is better, just putting it forth as an example of how things could be radically different than they are now (even for statically typed, compiled languages), had we made different choices. Consider the alternative, the situation we have now: we use a technique which could support anywhere from 1 to infinity polymorphic derived classes to implement interfaces which probably have approximately 2 different implementations in a typical finished program.
Perhaps an analogy would help: If we approached hardware the way we approach software, we would be using connectors which support between 1 and infinity pins everywhere when connectors which support N pins would suffice.
What polymorphism is good at is building the black box, the soft boundaries, and that isn't a good thing to do for fine-grained details. I do occasionally find a use for extension, but it's at a scale closer to "call this entire subprogram as a kind of state machine", vs "write this algorithm as a collaboration of objects abstracted away from each other".
The "alternate hard and soft layers"[0] pattern comes to mind: if what you really need is modularity, going towards full dynamism and reflection seems like a better choice than to try to extend everything statically, which creates a very large latency issue(the default response to every form of change in a static system is "recompile from the beginning, precompute all answers"). At the same time, it's not a good fit to have a vast codebase do everything dynamically since the comprehensibility and throughput suffer.
Your idea of replacing polymorphic variants with sum types sounds interesting. It also strikes me as quite similar to compile time polymorphism (e.g. generics) where you are 1 to infinity in the design space but the generated program boils down to only the variants created. Language wise I really like the mix of sum types, traits and generics in Rust.
It seems that the issue is less that the code is made more opaque but that you can quickly think of some counter-examples or nitpicks that make the advice less valid to you?
I think the problem of not being universal is true of most programming advice. So I don't necessarily see it as a reason to discount it. I also think in contrast to a lot of articles the author has done a great job to couch their advice as being contextual. For example for your criticism of Pattern 4 the author does actually point out the obvious solution to these complex expressions. To split them out into several parts. Which is what you get with if statements to an extent but spread out much more with a lot of clutter.
Some of your criticism also seems to be based on a misreading of the authors intent. For example Pattern 5 where the goal isn't to remove the if statement but prevent having to repeat the same error checking pattern everywhere.
My point is that this in itself is bad advice.
Overuse of oop concepts is just as harmful (if not more) as overuse of if statements.
But I'm fairly convinced the author isn't saying that they are to be generally avoided, to quote directly:
> If statements usually make your code more complicated. But we don’t want to outright ban them. I’ve seen some pretty heinous code created with the goal of removing all traces of if statements. We want to avoid falling into that trap.
The author's examples are not all about oop -- many of them having nothing to do with oop, they are just good advice for clean code in general.
fn box(status=checked)
do domething
fn box(status=*)
do something else
My Elixir projects have 0 to 10 ifs and they do solve real problems. I use plenty of ifs in other languages but I follow some of the advices of the post.Most pattern matching in languages is basically souped-up switch statements much of the time. Note that pattern matching is not one of the solutions mentioned by the author.
A Swift-enum allows you to have methods inside enums:
enum ConnectionStatus {
case connected
case connecting
case disconnecting
case disconnected
func `for`(isConnected: Bool, isConnecting: Bool, isDisconnecting: Bool) -> ConnectionStatus {
switch (isConnected, isConnecting, isDisconnecting) {
case (true, _, false): return .connected
case (false, true, _): return .connecting
case (true, _, false): return .disconnecting
case (false, false, _): return .disconnected
}
}
Then somewhere inside your code: public func connect() {
switch ConnectionStatus.for(isConnected: self.isConnected, isConnecting: self.isConnecting, isDisconnecting: self.isDisconnecting {
case .connected, .connecting: return // We're already good
case .disconnecting: reconnectWhenDisconnected()
case .disconnected: startConnection()
}
The amount of if's that have to be juggled to do the same and the resulting code that is hard to parse when you're maintaining it definitely is more problematic.Same goes for `Optional<T>` in an argument position, though https://github.com/quchen/articles/blob/master/algebraic-bli...
This blog post suffers from the classic problem of blog post code examples - to fit the example in a blog post it needs to be trivial, and because it is trivial it doesn't demonstrate the real value of these techniques.
I see if/else overused all the time - every single day in fact - when the techniques in the blog post should have been applied.
It was then my job to translate that into SQL which doesn't really have "if" in the same sense as an imperative language does.
Sure I could have pulled out all the data and done the branching in Python, but that would have been a performance hit, but mainly because it would be ugly, require a lot more code and likely be a lot buggier. These days I try to keep as much work done in the database because the declarative nature of SQL generally works out with a lot less bugs as well as the performance benefit.
Sure it's not going to be possible for every case, but I have noticed that the more experienced I get, the less I like "if"s.
If your code contains quite a lot of branching - especially the same pattern of branching in several places - then you should certainly consider replacing that with polymorphism, in the form of virtual methods or perhaps templates/generics (compile-time polymorphism). But you should also consider leaving the branching in place if that is simpler overall. That doesn't make the article bad advice IMO, you just need to not take it too seriously.
It’s annoying though (and kind of strange, really) that these articles (which discuss very basic concepts) make it to the front page, and may influence a bunch of rookie developers the wrong way.
As design theory? Fine.
As HN frontpage stuff? I mostly see it get discussed in the form of new devs or students asking things like "wait, how should I remove all the 'ifs' in my Java code?" (Yes, that's a real example.)
Spreading the good news about case statements, function passing, and so on is great. But this stuff is so often written up as absolutes, when simple conditionals really are a part of most practical cases.
As a first step, I'd say better to just isolate the branching in one place, as a separate function, rather than immediately jump to polymorphism which could have high refactoring costs in some cases (even if it might eventually be worth it).
Because premature abstraction is just as bad as premature optimization.
There's a notable difference between "if the box is checked, do [thing 1], otherwise do [completely different thing 2]" and "if the box is checked, do [thing 1], otherwise do [basically thing 1, but with this additional metadata from service y]."
And that difference, I think, is what tells you where to put the `if` statements. If things are distinct enough, you have some easy-to-read top-level ifs and switch based on that. Otherwise it's more complicated to trade readability vs code reuse/deduplication.
But where you get into big trouble is when you don't think about the dangers of if's at all, and end up with call stacks 5+ methods deep with no rhyme or reason to why certain things are done in if statements in level 5 and other things are done with if statements in level 1. Creating a mental map of what's done where in what conditions for that kind of code is really hard - as is thoroughly testing it, since this often means you're passing a lot of context really deep and don't have easily separable units. (And yeah, "Switch To Polymorphism," I think, is a risky one here - that can turn into "still have the deeply nested if statements, but make them invisible.")
I don't think the article really hits that well, though.
My attempted better advice would be to keep methods short and the nesting/indentation limited. Use good method names. Make it easy to understand each method in isolation.
Seriously are you saying that
if (x) {
return y;
} else {
return false;
}
is as easy to read as return x && y; <?php
$x = 1;$y = 2;
function test1($x, $y) {
if ($x) {
return $y;
} else {
return false;
}
}
function test2($x, $y) {
return $x && $y;
}
var_dump(test1($x,$y));
echo "\n";
var_dump(test2($x,$y));Since otherwise, it’s not a question of readability anymore. (But yes, incorrect transformations are incorrect.)
if isAdmin:
return false
if not isActive:
return false
return true
vs return (not isAdmin) and isActiveRegarding which way is better, there's not much benefit arguing one way or the other. This is a leaf decision. It doesn't affect the structure of other parts of the code.
It's been in nearly all languages, and for decades before python existed.
>>> 0 and 'asdf'
False
>>> 3 and 'asdf'
'asdf'
>>> 3 and ''
''
'and' and 'or' in python are short-circuited as in e.g. C, but it's even more short-circuited! The "last" component is not evaluated in a boolean context. For a complete evaluation you have to use the expression in an if or while statement or apply the bool() function. I would say it's like a monad, if that helps. if (a) return false;
if (!b) return false;
return true;
May be easier to read than: return !(a || !b); !a && b
? :)a -> isAdmin
b -> isActive
const isDeletableUser = !isAdmin && isActive;
return isDeletableUser;
(Better would be naming the function "isDeletableUser" instead of the intermediate const, but was keeping with the original.)Point is for me that booleans with readable names shows how eliminating ifs becomes not only doable but arguably preferable.
return a ? false : b;I’m a heavy user of ternary, but it’s still an “if”. And and Or are (not very much) higher-level operations.
The overall logic remains the same, it's really just giving one a clearer view of a specific path.
You need to look for a balance between ifs and alternative patterns.
Ifs exist because most real world problems require them. “If the box is checked, do this, otherwise do that”.
Masking if statements by syntactic sugar doesn’t serve any purpose in my opinion and if anything it makes the code more opaque, or worse, may force you to duplicate code...