Live data from Hacker News

Anti-if: The Missing Patterns

code.joejag.com

21–30 of 74 posts

Re: Anti-if: The Missing Patterns

#21
Uhm. No, no, no, no and no.

> [Pattern 1] Solution: Split the method into two new methods. Voilà, the if is gone.

And so is parametrization.

> [Pattern 2]: Solution: Use Polymorphism. Anyone introducing a new type cannot forget to add the associated behaviour.

OTOH you make extensions more complex. I usually write in lisp and DO NOT use polymorphism for this but etypecase in the base implementation of a method (switch/case where the default case automatically throws an error). This allows people who extend my code to implemented a method specific to a type that my base implementation handles and thereby overriding my implementation with minimal effort.

> [Pattern 3]: Solution: Use a NullObject or Optional type instead of ever passing a null. An empty collection is a great alternative.

This is, IMHO, not relevant at all - the decision how null values are handled simply has to be documented so programmers can make an informed choice. The only exception I accept is an empty collection which I'll always prefer over null, assuming that all other code handles empty collections gracefully.

> [Pattern 4]: Solution: Simplify the if statements into a single expression.

Works for the contrived example (and we all know that the code is really bad). For more complex code I rather follow the logic line by line instead of mentally disassembling a 10 line boolean expression.

> [Pattern 5]: Solution: Give the code being called a coping strategy.

No, just no. One does not aim at removing exceptions. Those are to be handled by a higher layer who actually knows how to handle it. If one uses default values one still has to check on that layer with the added burden of using a default value that is guaranteed to never be returned as a real value so we can make the distinction between record found/record not found.

Re: Anti-if: The Missing Patterns

#22
post #2

> Context: You have a method that takes a boolean which alters its behaviour > Problem: Any time you see this you actually have two methods bundled into one. That boolean represents an opportunity to name a concept in your code. This is only true if you're using literal booleans at you call sites. If the booleans are coming from somewhere else, like user input, you just moved your single if statement in the method ou…

Common Lisp to the rescue! (defmethod make-sound ((b bear) (loud-p t))) ;; bear makes loud sound ...) (defmethod make-sound ((b bear) (loud-p null)) ;; bear makes quiet sound ...) If we call (make-sound bear-instance nil), the second method is invoked because loud-p is specialized to the null class, whose only instance is nil. (There is a type nil also, whose domain is the empty set: it has no instance.) If we call (…

Are the semantics to this comparable to Haskell's pattern matching on function arguments?

Re: Anti-if: The Missing Patterns

#23

I'm disappointed that of all the patterns presented, most of them actually increase the complexity to some extent, and none of them are the use of a lookup table/array. IMHO that is the "real anti-if pattern" here, and it can immensely simplify code. I've taken long, convoluted nested if/else chains with plenty of duplication and turned them into a single array lookup. A bonus is that performance and size often benef…

I do this a lot too. Often long chains of if statements are really performing a manual mapping from one set of values to another. Just keep a map and perform a lookup! Easier to read, easier to change, and easier to extend since the mapping is a data structure instead of code.

Re: Anti-if: The Missing Patterns

#24

Getting rid of if/else is nothing but moving the if/else to somewhere else.

This is only true in a very technical sense.

    function getColor(str){
       if(str=='blue'){ return '#0000ff'; }
       if(str=='red') { return '#ff0000'; }
    }
    
    getColor = {
      blue: '#0000ff',
      red: '#ffoooo'
    }

The second one also has if statements inside the implementation of the hash map, but it isn't actually polluting your business logic. The question is no longer "what do you do when the input is like this or like that", but "lookup the corresponding output".

The distinction is nice because I know that getColor[thing] will likely have no side effects. I can treat it, on a theoretical level, strictly as a map.

Of course if the problem is "implement this on a machine without branching" this isn't super useful but most problems are not that.

Re: Anti-if: The Missing Patterns

#25
All of the proposed solutions are deficient:

> Pattern 1: Boolean Params

This solution suffers from what we typeful programmers call “Boolean blindness”: https://existentialtype.wordpress.com/2011/03/15/boolean-bli... . When you compute a bit, what you are actually interested in is the meaning of the bit, which isn't included in the bit itself. For example, if the bit is set, then “x” is less or equal than “y”; if it's unset, then “x” is greater than “y”.

> Pattern 2: Switch to Polymorphism

Dynamic dispatch (what the author wrongly calls “polymorphism”) can be used for open-ended case analysis. But it suffers from two drawbacks: (0) Unless you have multimethods, case-analyzing two or more values at the same time is a bitch! (1) Even if you do have multimethods, the open-ended nature of this whole business makes static exhaustiveness checking (making sure that no case is missing) impossible.

> Patte[r]n 3: NullObject/Optional over null passing

Null objects are still falling back to pattern 2, and optionals are severely underpowered in languages without pattern matching and compile-time exhaustiveness checks. For instance, one of my favorite patterns is implementing operations (say, “merge”) on non-empty collections, then extending them to possibly empty ones:

    (* pe = possibly empty *)
    datatype 'a pe = Empty | Cons of 'a
    
    (* extend a merge operation on non-empty collections
     * to possibly empty ones *)
    fun pointed _ (xs, Empty) = xs
      | pointed _ (Empty, ys) = ys
      | pointed op++ (Cons xs, Cons ys) = Cons (xs ++ ys)
    
    fun merge (xs, ys) = ... (* possibly complicated *)
    and mergePE args = pointed merge args
How do Java-style optionals help?

> Patte[r]n 4: Inline statements into expressions

This only makes the problems associated to Boolean blindness even worse.

> Pattern 5: Give a coping strategy

What if there is no sensible default value? Just... no.

---

What you really need is algebraic data types, pattern matching and exhaustiveness checks.

Re: Anti-if: The Missing Patterns

#26
post #7

I'm afraid the article has left me unconvinced. I'm open to having my mind changed on the matter, though. The point of passing boolean params instead of named functions is that most of the time there is shared code between the two paths and not literally all the code is enclosed in either the if or the else block. If the author was intending just to restrict to that one specific case where there was no overlap whatso…

> I don't really see how using an Optional type would remove the if(null) checks. It just makes the meaning explicit, which is good but not remedying the original problem. If you always use `Option` (or whatever equivalent your language/base library provides) and banish `null` from your source code you've effectively avoided all null pointer exceptions and can therefore remove all null checks.

Well, no, you still need the null checks. You've eliminated null pointer exceptions by guaranteeing that null is always checked for before the value is used -- you're still checking, but you're now immune to unanticipated crashes. (That would have resulted from null dereferences, that is. You can still have other bugs.)

Re: Anti-if: The Missing Patterns

#27
post #7

I'm afraid the article has left me unconvinced. I'm open to having my mind changed on the matter, though. The point of passing boolean params instead of named functions is that most of the time there is shared code between the two paths and not literally all the code is enclosed in either the if or the else block. If the author was intending just to restrict to that one specific case where there was no overlap whatso…

> The point of passing boolean params instead of named functions is that most of the time there is shared code between the two paths and not literally all the code is enclosed in either the if or the else block

If you have this

    method(bool arg)
    {
      // block A
      if (arg)
      {
        // block B1
      }
      else
      {
        // block B2
      }
      // block C
    }
you can replace it with

    method1()
    {
      A();
      B1();
      C();
    }
    
    method2()
    {
      A();
      B2();
      C();
    }
where A(), B1(), B2() and C() are new methods that contain the previous blocks. This makes the code clearer, IMO.

Re: Anti-if: The Missing Patterns

#28
post #7

I'm afraid the article has left me unconvinced. I'm open to having my mind changed on the matter, though. The point of passing boolean params instead of named functions is that most of the time there is shared code between the two paths and not literally all the code is enclosed in either the if or the else block. If the author was intending just to restrict to that one specific case where there was no overlap whatso…

> The point of passing boolean params instead of named functions is that most of the time there is shared code between the two paths and not literally all the code is enclosed in either the if or the else block If you have this method(bool arg) { // block A if (arg) { // block B1 } else { // block B2 } // block C } you can replace it with method1() { A(); B1(); C(); } method2() { A(); B2(); C(); } where A(), B1(), B2…

>This makes the code clearer, IMO.

Depends on how much state you need to pass between A, B1, B2 and C.

Re: Anti-if: The Missing Patterns

#29
I had a professor in university who was frequently saying he developed a thousand application and he didn't use any if.

I know it's extreme but is there any way to reduce (lets say 5 to 1) conditions?

I think he was developing Fortran apps.

Anecdote: This very same professor asked us to do a matrix operation (I don't remember what) without using if once. He said it would be faster than using ifs. Many of us couldn't. Then he revealed his solution, he was getting first indice of first row and doing some operation and getting the last indice of last row, then second indice of first row and so on. One of my friend told that if he wrote the algorithm plain using ifs, it would be faster. Professor told it's impossible.

In the end of the day the version which was written using ifs was a lot faster than the version without ifs. Because of the changing indices cause a lot of cache misses but getting matrix elements sequentially used cache correctly.

Re: Anti-if: The Missing Patterns

#30
post #23

I'm disappointed that of all the patterns presented, most of them actually increase the complexity to some extent, and none of them are the use of a lookup table/array. IMHO that is the "real anti-if pattern" here, and it can immensely simplify code. I've taken long, convoluted nested if/else chains with plenty of duplication and turned them into a single array lookup. A bonus is that performance and size often benef…

I do this a lot too. Often long chains of if statements are really performing a manual mapping from one set of values to another. Just keep a map and perform a lookup! Easier to read, easier to change, and easier to extend since the mapping is a data structure instead of code.

Efficiency-wise I agree, but if I don't care about efficiency then I often prefer an if-chain (or better still, a switch-case).

The point is that the desired mapping is known not just at compile time, but is explicitly known to the programmer. So it is clearer if it right there in the executable code rather than hidden away in some moving part (the mapping data structure).

Post reply on HN