Writing branchless code
(or at least reducing the branching of your code to a minimum)
I saw the above picture in a reddit post the other day (link) and thought to myself: “Really? Is that the best we can do? Choosing between two types of branching in our code?” Of course not. We can try to eliminate or reduce it to a minimum first, only then choose between types of code branching. Thus my response to the post:
First of all, let me state why branching is bad. It is bad for a number of reasons:
- In the first place it is hard to read when used excessively, which directly leads to
- A higher chance of creating bugs. Even if you take care to not create that bug, it means you will spend a considerable amount of time to understand all the code paths before changing anything. And those paths can really be a tangled mess;
- Makes your tests harder to set up. It will take more time and effort to hit the execution path you aim to test.
- Leads to test duplication. You will need different tests to hit the different paths of branched code. Everything outside of the branched paths is duplication.
- Looks ugly when it introduces indentation (mainly applies to if/else and switch/case).
Some practitioners who have heard that if
statements are bad, try to replace them with ternaries (the ?:
operator), or even with logical AND/OR. The latter I have noticed in the more recent years. Well, they are all still branching of your code. The logical operators are even worse than all the others, because they are like hidden branching, really hard to spot — so instead just please use an if
statement, at least the branching will be obvious to the ones reading your code.
Here we come to the ways to eliminate/reduce the branching in your code:
- Use polymorphism. Maybe you have heard about replace conditional with polymorphism. It can’t eliminate branching completely, but can limit it to just one occurrence. If you wonder where that would be — it is in the code which is responsible for creating the instance of the correct subclass (usually a factory).
- Return early. The following:
if (condition) {
f1();
f2();
f3();
f4();
f5();
} else {
f0();
}
can be re-written like this:
if (!condition) {
f0();
return;
}f1();
f2();
f3();
f4();
f5();
and the else
clause is eliminated.
- Initialize stuff whenever possible. This one has two aspects:
- You can use initialization to eliminate
else
clauses. Consider this:
let color: string;
const losses = someAPI.calculateLoss();if (losses > 0) {
color = 'red';
} else {
color = 'green';
}
It can be done like this:
let color: string = 'green';const losses = someAPI.calculateLoss();if (losses > 0) {
color = 'red';
}
2. You can use initialization to skip null checks:
let timer: NodeJS.Timeout | undefined;// somewhere in your code you will set it
timer = setTimeout(() => f(), 1000);// and when you want to clear it you have to null check it
if (timer) {
clearTimeout(timer);
}
Instead you can initialize it:
let timer: NodeJS.Timeout = setTimeout(() => {});// somewhere in your code you will set it
timer = setTimeout(() => f(), 1000);// and when you want to clear it you can do so unconditionally
clearTimeout(timer);
- Use expressions which return
boolean
. Example:
let found: boolean;
const index = array.findIndex((item) => item > 0);if (index >= 0) {
found = true;
} else {
found = false;
}
the above should be:
let found: boolean;
const index = array.findIndex((item) => item > 0);found = index >= 0;
- Use
Array::filter
. When you need to perform two different actions to your array items based on a condition you usually would do:
const array = [1, 2, 3, 4, 5];for (const item of array) {
if (item % 2) {
console.log(`${item} is odd`);
} else {
console.log(`${item} is even`);
}
}
However, what if I told you you don’t even need if
or switch
statements:
const array = [1, 2, 3, 4, 5];
const odds = array.filter((item) => Boolean(item % 2));
const evens = array.filter((item) => !Boolean(item % 2));odds.forEach((odd) => console.log(`${odd} is odd`));
evens.forEach((even) => console.log(`${even} is even`));
- Use dictionaries. The following
switch
statement:
const add = () => {};
const delete = () => {};
const update = () => {};function act(action: string) {
switch (action) {
case 'add':
add();
break;
case 'delete':
delete();
break;
case 'update':
update();
break;
default:
throw new UnknownActionError(`Unknown action ${action}.`);
}
}
can be replaced by the following dictionary:
const add = () => {};
const delete = () => {};
const update = () => {};const actions: { [key: string]: () => void } = {
'add': add,
'delete': delete,
'update': update
};function act(action: string) {
try {
actions[action]();
} catch () {
throw new UnknownActionError(`Unknown action ${action}.`);
}
}
Some branching rules to live by
As we saw above branching cannot be always eliminated completely. But what is OK and what is not? Here are my personal standards:
- No nested
if
s,else
s,switch
es, ternaries. If you end up here it is most probably a problem on a greater (design) scale. - No logical AND/ORs used for code branching. It doesn’t eliminate the branching, even worse — hides it.
- No
else
after anif
statement, unless both theif
andelse
paths need to merge to the same path after completing. So 99% of the time it will be a singleif
with an earlyreturn
inside. The other 1% of the time there will be anelse
clause after theif
.
So there you go — code branching is a cancer and we should do our best to keep it to a minimum, or at best — eliminate it completely. Share your ways in the comments below, as I am sure I have just scratched the surface here.