Sign Up Today
☀️ 🌙

Arch-Engineer

Software Design Tip: The Real Way to Eliminate Conditionals from Your Code

(and how to avoid deceiving yourself)

I'm a supporter of the Anti-If Campaign, or at least of their premise: if-statements make code more complex and should be avoided. They can create an exponential number of paths and cases, and you must somehow guarantee your code works on each one. Indeed whenever I write a case study about a new codebase, I start by grep'ing for if-statements, knowing that, wherever I see them concentrated, a design flaw I can write about is sure to be nearby.

Software bloggers have agreed, and produced a lot of articles on specific techniques to reduce if-statements. You could spend weeks reading these, and still not learn anything that applies to your case.

Fortunately, you can save a lot of time by understanding that all of these reduce into two fundamental ways of eliminating if-statements.

  1. Moving a test over an unknown value to a place where it is known.
  2. Changing your explanation of how the code works to not be conditional.

As an example of the first, consider a codebase that deals with latitude/longitude coordinates. Every place that uses them must do a number of checks to make sure they're valid: latitude must be between -90 and 90, longitude must be normalized to between -180 and 180, and no values may be NaN. These many if-statements can be eliminated by replacing the raw floating point numbers with a dedicated type for latitude/longitude coordinates, which is guaranteed to be well-formed. Any code you write that generates latitude/longitude coordinates generates this type. Doing so moves the check that the coordinates are well-formed to the code that generates the coordinates, which can guarantee they're well-formed. The check can hence be eliminated. If you input latitude/longitude points from outside the system, then you can use a single round of checks to convert them into your dedicated type at the boundary, effectively "remembering" that you have already checked for validity.

The famous technique of replacing a conditional with polymorphism (or a lambda) falls into this category, as does replacing a boolean parameter with two functions.

For the second category, consider an e-book application. When a user opens an e-book, you look up the last page they've read; if there is none, you show the first page instead. This conditional can be eliminated by using some kind of getWithDefault method instead — or even by having a nextToRead field initialized to 1. In this example, we've changed the code's algorithm from "Get the last page read, or page 1 if this is the first time opening the book," to "get the next page to read," which is defined as page 1 for the first read.

The null-object pattern is a popular example of this category.

So there's no need to memorize a catalog of quick-fixes. You just need to understand these two general approaches, combined with the general skills of making small changes that preserve semantics, and being able to phrase and rephrase the intention of your code.

Self-deception

It's easy to fetishize the idea of eliminating if-statements, and find all kinds of tricks that seem to do so for huge classes of examples.

Unfortunately, it's very easy to deceive yourself. Conditionals are a semantic notion, not a syntactic one, and there are plenty of ways to remove the word "if" from your code without changing the underlying logic. There is no general way to eliminate an if-statement without needing extra context.

In my Strange Loop talk, I showed one case of using an array access to hide a conditional, and the deeper sense in which the conditional is still present. In the first week of my web course, students work through an example of hiding an if-statement using arithmetic tricks, working out the logic in full detail.

Today, I encountered a sad case of a proud blogger who stumbled on another way to hide if-statements, proclaiming it a "one weird trick for eliminating many conditionals." His idea: instead of having a function that returns one of several values, make it call one of several callbacks.

This raises flags immediately for being too general: it can be applied to any if-statement whatsoever. For example, this:

if (x > 0) {
  print("positive");
} else {
  print("negative");
}

becomes this:

ifElse(x > 0,
       () => print("positive"),
       () => print("negative"));

and it hence becomes clear that the conditional has not been eliminated in any meaningful sense. What he has instead done is rediscovered a technique that goes back to the 30's for replacing any data type with functions, without any deeper structural changes.

The complexity of code comes not from the text used to print it, but from the explanation of why it works. If your explanation of why the code works has not changed, then any change you've done to the code is cosmetic.

This also illustrates the importance of learning type theory. Imagine a tinkerer trying to create a perpetual motion machine. To him, every combination of levers and magnets and wires is a new chance to succeed — until physics comes and rules out all of them. Similarly, it can feel like there's an infinitude of options in altering code, but type theory teaches us that we actually have a limited toolbox, and all options can be explained in terms of a limited set of well-understood techniques.

Learning to see this extra layer of meaning is one of the most important skills of a software engineer.