Live data from Hacker News

Anti-if: The Missing Patterns

code.joejag.com

61–70 of 74 posts

Re: Anti-if: The Missing Patterns

#61
post #58

I'm surprised there was no mention of the Specification pattern. https://en.wikipedia.org/wiki/Specification_pattern

My god what an abomination of programming simplicity. Do people actually write code like this? It's as if someone said you must use interfaces and you must use objects for even the simplest boolean logic statement. I think I would claw my eyes out if I had to read and write code like this on a regular basis.

Re: Anti-if: The Missing Patterns

#62

Earlier quoted context omitted.

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

If the amount of state you need to pass is that significant, that is another code smell that needs its own resolution.

Re: Anti-if: The Missing Patterns

#63

Earlier quoted context omitted.

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

You need checks but only in the cases where you need checks. More importantly, nullable values are very obvious in the code (and should very much be the exception rather than the rule), so the cases where you need to think about "what if that's null" are cases you actually think about.

Re: Anti-if: The Missing Patterns

#65
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 the author was intending just to restrict to that one specific case where there was no overlap whatsoever, then I guess I would agree but I'm not sure how often that happens.

The cases where there's a lot of commonality are cases for polymorphism instead. If you have something like:

     def doSomething(useCache: Boolean) = {
       //long and complicated function
       if(useCache) cache.lookup(foo) else calculate(foo)
       //more long and complicated stuff
     }
then it's probably worth breaking that out as

    trait FooProvider { def get(foo: String): Foo }
    object CachedFooProvider extends FooProvider {
      def get(foo: String) = cache.lookup(foo)
    }
    object CalculatingFooProvider extends FooProvider {
      def get(foo: String) = calculate(foo)
    }
and passing the FooProvider instead - that way you pass something with a clear semantic meaning rather than a bare Boolean.

> I'd say that at least this trivial example would be best handled by some data-driven approach. Look up salaries in a database instead.

Very much disagree. Every time I've seen a program do logic based on what was in the database it's been very hard to debug or reason about. Anything you can possibly do to make it unit-testable instead of needing to test against a prod database dump is worth it.

> Surely this is easier to misread than the if statement example, which in my mind closely matches how I think.

Disagree. The if looks like control flow when it's not actually control flow. If you're just doing Boolean logic, make it look like Boolean logic.

> But even then, that only works when the actual return type is boolean and there are no state changes made.

Yes, that's exactly the point. It's well worth forcing those things into different functions, so that you can inspect what's going on. Particularly when debugging, that means you can step over the conditional and see what the result was, rather than having to step through each if because you never know when one of the branches might actually do something.

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

Optionals have polymorphic methods so you can express most common use cases directly rather than having to branch, i.e. rather than:

    val user = if(null == userId) null else userRepository.lookup(userId)
you can just do

    val user = userId flatMap userRepository.lookup
map/flatMap/foreach each have their own specific semantics so it's easy to see what's going on, whereas an "if(null == userId)" could mean anything. (If your logic really doesn't fit into any of the standard use cases then you may need to use an if even with an optional, but such cases will stand out when reading the code - as they should).

Re: Anti-if: The Missing Patterns

#66
post #12

If you want extreme case of removing all ifs, see how ifTrue: and friends are implemented in Smalltalk (ie. true and false are instances of different classes), although essentially all compiler implementations turn that into normal conditional branches in generated bytecode.

I think I'm looking at an implementation of ifTrue[1], but it's a bit dense to someone who's not familiar with Smalltalk. Maybe you could walk me through it?

[1] - http://git.savannah.gnu.org/gitweb/?p=smalltalk.git;a=blob;f...

Re: Anti-if: The Missing Patterns

#67

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…

"turned them into a single array lookup."

Midway between nested conditionals and lookups is using flags and a tablesque series of conditionals. Redundant but easily understood (and therefore testable).

I first learned about this technique from Code Complete (linked in the TFA). IIRC, McConnell used the term "truth tables".

"...since the code is far less branchy."

Code construction GoF style Design Patterns just introduce another level of indirection. That's it. (Insert cliche here.)

"Design Patterns" are overused, especially in the Java world (eg J2EE/Spring, Service Providers, Strategies, DI, IoC).

The "design" part is figuring out the least complicated balance between conditional branches and call stack depth, between composition and inheritance.

Rant over.

Re: Anti-if: The Missing Patterns

#68

Earlier quoted context omitted.

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

switch-case is often compiled to a table lookup, but if your expressions are constant (at least for the part where the table is being used), the latter is definitely less verbose. So it is clearer if it right there in the executable code rather than hidden away in some moving part (the mapping data structure). Following several levels of nested if/else is "clearer" than going to the right array index? I don't know wh…

If you have several levels of nested if-else (as opposed to a linear chain of "if"-"else if"), then you probably need a more complicated table than a linear array.

The moving appart that gets hidden away is the table (or more often, Hack mandictionary) that is stored somewhere in memory. If you are lucky, it is stored statically. It is never defined in quite the same place as the decision that it implements.

Re: Anti-if: The Missing Patterns

#70
post #9

> Patten 3: NullObject/Optional over null passing The solution here disperses the responsibility of keeping sumOf safe to its callers, all over the place, instead of a single location in sumOf.

Agreed. The sumOf function is now made more brittle by that fact, and would be unworkably so in a publicly-available API. This is easily fixed by a try-catch block if one finds that they are indeed allergic to if statements, although that too is a form of flow control. If you are programming in Go, you're going to have to bite the bullet and use an if. (I suspect the author would prefer jumping in a lake over program…

It is more brittle, sometimes the best you can do is to prefer empty collections over null. Some environments do offer a solution by using parameter annotations. Personally I do a lot of C# with resharper where you can add the [NotNull] attribute to a parameter, which enables some compile time checking.
Post reply on HN