Standardize the Way You Write Your Range Conditionals

By always spelling your range conditionals start <= index < capacity, you make your code more easily grokkable and reduce the chance of errors.

by on

An old memory resurfaces

The other day I encountered the following lines of code during a review.

if curr_page + 1 >= total_pages:
   # curr_page is out of bounds, do something about it.

It took me a few seconds of mentally compiling and running through the code before I could agree that it was correct. That moment of effort felt unfamiliar, though faintly nostalgic, like hearing a song that was popular during high school on the public address in a supermarket. And just like a one-hit wonder from the nineties, range checks or conditionals ordered like these had vanished from my personal landscape until this chance encounter.

When we first start programming, simply learning the concepts of loops and stopping conditions is daunting enough. The skills required to optimize our code for readability and long-term maintainability are not explicitly taught at this point, and nor should they be, for risk of overburdening the student. A lucky few of us might be exposed to well-crafted code, and pick up some of these practices through copying the patterns we see. Even then, such practical knowledge lacks the depth to convey why these patterns are useful. This means that many programmers begin their professional life producing less-than-readable code.

Prevent Fencepost Errors with This One Weird Trick

Over time, I had unconsciously learned that my code was less buggy and more readable when I formatted my range conditionals a certain way. The right way, I had found, was to spell them:

start <= index < capacity

The foregoing snippet is in Python, and takes advantage of comparison chaining. In Go, this may look like:

start <= index && index < capacity

The key points are that:

  • The comparands are ordered from left to right, in non-decreasing order, and
  • (for zero-based numbering): the check with the lower bound is always less-than-equals, while the check with the upper bound is strictly-less-than.

In these snippets, start, index and capacity are all meta-syntactic variables, or stand-ins for the actual variable names in your own code. Additionally, you may not need to have both sides of the range check. Your own conditionals might look like:

assert 0 <= index # Checks that the index is at,
                  # or exceeds, the lower bound.
for (i = 0;
    i < 10;  \\ There is the high bound check.
      	     \\ We loop if the index is below it.
    i++) {
while (pager.getCurrentPage() < pager.getTotalPages()) {
if !(start <= i) {
   return nil, errors.New("i index too low.")
}

The Benefits

Why pay the effort of memorizing and applying a rule like this? Because, by standardizing on this spelling, you can be more confident about the code you write, and debug it at a glance.

The goal of formatting code the right way is to make mistakes look obvious. Through repetition, you train your eyes to parse the code at a higher level than just the individual tokens. <= i and i < become the chunks that you look for, and any deviation from that pattern becomes something to be investigated further, and/or commented upon.

Contrast this with the alternative. These are all valid ways, though less readable, ways to spell 0 <= i:

0 <= i
-1 < i
i + 1 > 0
i >= 0
!(i < 0)
i > -1

By failing to format this code in a standard way, we rob the reader of the ability to pattern-match. Every reader then needs to parse each different arrangment of a range check anew, both to make sense of the code, and to verify that there are no errors. Testing the code that checks the bounds of loops is notoriously difficult, and so we need all the help we can get in this regard.

If i == done; break

In conclusion,

  • the spelling of the inequalities of your range conditionals has implications for the readability and correctness of your code.
  • Always order your checks so that the lowest is to the left, and the largest is to the right
  • Assuming zero-based indexing, use <= on the low side
  • Assuming zero-based indexing, use < on the high side

Finally, I'll leave you with this article by E. W. Dijkstra, which makes use of these range conventions, and argues that numbering should start at 0.

Other articles you may like

This article was filed under:
Programming Craft