“Arrow Code” refers to a series of nested
if statements that increase your indentation level at each statement, creating an arrow of whitespace when the
ifs are mirrored by accompanying
While arrow code arises naturally from the way our brains work through problems — “if this is true, and if that is true, and if this final bit is true, then finally do this” — they make our code less readable. Readability counts in programming. Code is read more than it’s written, and code that is easy to read and understand is code that is easier to modify or extend without errors.
While I was working on a task today I realized it would make a great example of how to flatten arrow code, so I’m going to share it with you. This example uses Jeff Atwood’s 3rd and 4th flattening tactics.
This is also an example of how the logical structure of arrow code isn’t always manifested as a physical arrow but can be hidden in complex boolean conditional statements. While the physical structure is different, the problems and the logical structure of the solution are the same.
I needed to determine if something was open based on three checks on it’s value:
- It had to exist
- It couldn’t be
- It couldn’t have a certain string (
'Provider') in its value
The full arrow approach would be to make each one of these its own
if value: if value.casefold() != 'closed': if 'provider' not in value.casefold(): site_open = True else: site_open = False else: site_open = False else: site_open = False
Admittedly, that arrow is contrived for the sake of physically showing the arrow. It’s pretty easy to realize that we can combine the
if’s onto one line, and because the
else case is the same for each
if we can just set it at the beginning and get rid of the
site_open = False if value and value.casefold() != 'closed' and 'provider' not in value.casefold(): site_open = True
However, I would argue this is still not super readable. It still scrolls horizontally, and becomes a little hairy to maintain as you get more and more conditions. While it’s not physically an arrow, the logical structure is still the same.
In my prototyping today this was the conditional to a dictionary comprehension, which made it even more dense and difficult to quickly understand.
If you’ve not seen it before (it was new to me when I first saw it),
casefold() is a more internationalization-safe form of making a string lower case. Not strictly necessary here, but a habit I’ve gotten into.
So how do we fix it?
def is_open(value): if not value: return False if value.casefold() == 'closed': return False if 'provider' in value.casefold(): return False return True
First, I extract this logic into its own function. This not only allows us to use
return to great effect, but also helps isolate this bit of potentially non-trivial logic for testing (ideally, with it’s own series of unit tests).
Unit tests are particularly helpful with these kind of complicated conditionals. They both give you another chance to make sure your logic is correct and, if you need to expand the conditional, they give you the confidence that your earlier test conditions still perform the way they should.
Next, I invert the checks to use as many positive checks as possible. Instead of one positive check (
if value) and two negative checks, we now have one negative and two positives. This gets rid of the potential brain twisters of
not in, making it easier to understand what the
if statement is testing.
In addition, the
if not value existence check is common enough that it’s easily understood and using this inverted form doesn’t hurt readability too much. In a more complicated program we could be enforcing the idea that
value should never be empty (
'', etc), so this could also be a gateway check that raises an error instead.
return from the function as soon as possible instead of chaining the comparisons together or using an
if/elif/else switch. Because any one of our checks indicates that the site is not open, I can bomb out as soon as one fails. Otherwise, if it passes all the tests, I know that it’s open so I can just
This pattern of returning early may not seem as readable to you if you’ve not encountered it before or are used to just a single return at the end of a function. But, once you’re familiar with it, the pattern becomes easy to pick out and improves code comprehension. We now gain the advantage of being able to easily add new conditions without risking missing an
and or confusing it with an
As a bonus, each condition is now functionally independent from the others. This reduces the opportunity for weird interactions and side effects, and we can also test each one individually.
So there you have it. Flattening arrow code (even if it doesn’t originally look like a full arrow) will make your code easier to read, easier to understand, and easier to extend. If you don’t want to use the hammer, just save on postage and use the extract of llama instead.
This inversion of the conditionals and changing the
and into an effective
or (because only one of our
ifs need to be true to cause it to
return) is an application of De Morgan’s law.