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

I disagree with the comments here. This is mostly not good advice.

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...



I was wondering which of the examples you think particular fall into this category of making the code more opaque?

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.


I still "believe in" OOP, but there's something that is increasingly troubling me lately: I keep thinking about the difference between the unbounded set of possible derived classes which could be substituted for a base class, versus the small (probably 1-3) number of different actual implementations are ever used in a real program. It seems like this disparity indicates the mechanism we're using is overly general. Maybe this is an argument for Sum Types as opposed to runtime polymorphism.

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.


Sum types are a huge benefit to branchy code. Philosophically, OOP polymorphism can do a similar thing, but it doesn't really cover it as succinctly: if your operation is of the "similar data, mostly the same algorithm but with variations at points depending on the data's type", going down the polymorphic route requires a lot more naming of things to cover each variation, and then when you go to inspect it, the code is twisty and jumps all over the place. I've had a lot of bugs introduced by method boundaries obscuring the execution flow.

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.

[0] http://wiki.c2.com/?AlternateHardAndSoftLayers


I didn’t mean to do down OOP. I think there are a lot of useful concepts to be had looking at it. Polymorphism for extension is definitely one of those. It’s more a lot of the more ‘interesting’ takes I’ve seen to replace ifs come from that direction and seem to be an exercise in decreasing complexity in the small (easier to understand methods) whilst increasing it a lot in the large (harder to understand architecture).

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.


Sorry I answered as a top level comment. Im on my phone


No problem!

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.


Well despite a few sentences like “remember, if statements are not all bad”, the author still makes a case that if statements are generally to be avoided.

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.


Overuse of anything is generally harmful (since it suggests doing too much of it) otherwise it would just be use!

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.


> Overuse of oop concepts is just as harmful

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.


It depends on the language. Languages with pattern matching usually solve the problem with code like this (pseudocode)

    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.


I don't consider this style of pattern matching to be semantically different than ifs or switch cases. There's literally no "if" but they are fundamentally similar to having a single function with a switch/if inside for the "doing something".

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.


`switch boolValue` insists you do something with the false part, `if boolValue` does not. This is just looking at it from the most atomic level possible. If you have a group of booleans you get pretty complex lines of code. Switching on a tuple of three boolean where you can have cases like "if the first two are true ignore the third one" is super powerful. It becomes even better if you return an enum that indicates what it really means. Like CONNECTED, CONNECTING, DISCONNECTING, DISCONNECTED. Now every possible meaningful value of those three booleans has a word attached to it.

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.


Whose comment are you responding to? It seems you agree with my comment about pattern matching and switch cases.


I must've misunderstood your comment. I thought you were saying these pattern matching rules were basically souped up if's. Elixir has pattern matching on function level but it's also heavily ingrained in the whole philosophy. It's quite the antithesis to Java.


The don't care _ is doing a lot of work keeping that code less verbose.


In haskell passing a bool is still considered an anti pattern, it's called boolean blindness. This is mostly because bool's don't carry semantic information.

Same goes for `Optional<T>` in an argument position, though https://github.com/quchen/articles/blob/master/algebraic-bli...


Of course there are legitimate cases to use if/else.

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.


My previous team leader was a pretty poor programmer and would always describe the output of SQL queries he wanted me to generate with lots of "if, then".

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.


All rules for how to code better should be considered but potentially ignored while you're coding in practice. This includes the rule in the article ("never use if", although it doesn't really say that) and the rule in your comment ("never replace if with polymorphism", although this too is an exaggeration of what you said).

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.


Fair enough.

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.


Eh, there's no shortcut to experience. Beginners are going to get ideas from _somewhere_, without being able to thoroughly judge them. And they'll try some, and eventually they'll figure out what works and what doesn't. I thought this article was reasonably clear that it did not offer a silver bullet, just some structures to watch out for, consider, and maybe improve.


I would rather have new developers think in a more object oriented or functional way instead of procedural. If statements imho promote procedural thinking, while pattern matching, polymorphism, or to a somehow lesser extent, if expressions promote a more OO or functional way of thinking.


I agree that this stuff is mostly counterproductive.

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.


> especially the same pattern of branching in several places - then you should certainly consider replacing that with polymorphism

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.


> Ifs exist because most real world problems require them. “If the box is checked, do this, otherwise do that”.

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.


Yeah, as if a line with condensed conditional logic using Boolean operators really got rid of the branching and the running it through your head.

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.


I guess if you’re not familiar with Boolean operators that would be a thing?

Seriously are you saying that

    if (x) {
        return y;
    } else {
        return false;
    }
is as easy to read as

    return x && y;


Depending on the language and values, the latter does not mean the same thing.

    <?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 we’re discussing readability, let’s be generous and assume the code behaves the same.

Since otherwise, it’s not a question of readability anymore. (But yes, incorrect transformations are incorrect.)


I agree, there shouldn't be any argument on boolean logic being easier than the branching logic. The other examples are more discussion worthy.


I'd definitely say it was. The branch is much clearer and more explicit.


Branches are often important things to consider such that hiding them inside in-line conditional expressions can make a code reader miss a key piece of logic. Use in-line branches with caution.


The former can be understood by anyone with a basic grasp of English grammar, while the latter requires understanding the Boolean operator. So yes, the former is more readable.


Borrowing the variable examples from another comment, do you believe that the former is easier to understand for someone who knows English grammar than the latter? (I'm using Python syntax since we're going for English-like readability and added parantheses to the boolean operation to indicate order of operations for the liberal arts major that we've decided is the target audience of this code):

  if isAdmin:
    return false
  if not isActive:
    return false
  return true
vs

  return (not isAdmin) and isActive


If someone doesn’t have a basic grasp of Boolean logic then maybe programming is not the right choice..


Doesn't matter. Unless one option leads the compiler to generate better code than the other, it's a matter of aesthetics, not correctness. And since you don't even need to be a programmer to understand what the first one means, it's still easier to read.


An observation that comes from studying foreign languages: you learn “and”, “or” and “not” a long time before “if”.


Earnestly: Yes. But there may well be programming tasks or jobs that they are suited to, for which no understanding of boolean logic will make no difference on a day to day basis.


Huh, is this python-style logical evaluation, i.e. "x && y" need not be a boolean but could be whatever "y" is?

Regarding 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.


I'm curious, what about "x && y" is "python-style" ?

It's been in nearly all languages, and for decades before python existed.


It was not about "x && y" but about the construct from the parent post as a whole. It never uses y in a boolean context, but only as a value. So I was thinking y could be anything. In python this is how it works:

    >>> 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.


What you are talking about has nothing to do with short circuiting.


Agreed, bad choice of words. It's only somewhat similar. Maybe "lazy" would be a better term. Whatever, man! I hope it's clear what I meant.


It’s not that you’re unclear, it’s that it is irrelevant to the discussion, as you were talking about a fairly trivial and unrelated language feature — and it is also a feature that exists in many languages, hardly unique to python.


Whatever, man. Maybe it was not totally related. Agreed. Maybe I had a bad day. It started as a single line that wasn't even the point of my comment. Ok?


No, but:

    if (a) return false;
    if (!b) return false;
    return true;
May be easier to read than:

    return !(a || !b);


But is it easier than

    !a && b
? :)


Rename a and b to reasonable names:

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.


Seriously, it took me something like 10 seconds to parse your parent post. How a negated chain of if else would ever be easier to understand than a much simpler !a && b really defies my understanding...


What about:

    return a ? false : b;


I still prefer the Boolean logic version.

I’m a heavy user of ternary, but it’s still an “if”. And and Or are (not very much) higher-level operations.


I was being mostly facetious.


De Morgan laws are seriously your friends when trying to make complex boolean expressions optimally readable.


I agree with you on the keep indentation / nesting limited, but I am not a big fan of splitting things into short methods for the sake of it. Provided indentation isn't going too deep I often find it easier to read things in a script like manner instead of jumping in and out of lots of shortish methods.


I completely agree with you for the case when you say that avoiding an if brings you to code duplication. In that case from my point of view it’s a big NO. But in all the other cases, if you are using an OO language, there are always better tools than ‘if statements’. If you are using functional programming with if expressions instead of if statements then the ‘best balance’ moves a bit in favour of the ifs or a lot towards exhaustive pattern matching. On the whole I would say that I agree with the article discussed here, but with some caveats.


If you really think it doesn’t serve any purpose this means you should always use if and never the proposed constructs. This is clearly wrong and I’d recommend you rethink your position.


I don't think the argument is that the constructs are useless. The argument is that it's useless (not good advice) to use them if it is only for the purpose of avoiding if statements.


As per another commenter I used Elixir for 6 months a couple of years ago and very quickly started using pattern matching and guards to provide most of the If-type control flow. It didn't cover every case but it came very naturally in most cases where it was useful.

The overall logic remains the same, it's really just giving one a clearer view of a specific path.


Like anything else in life it is about balance. If you abuse ifs, then your code is unreadable, hard to debug and hard to maintain. If you abuse the alternative patterns then your code, again, becomes hard to grasp and hard to maintain.

You need to look for a balance between ifs and alternative patterns.




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

Search: