The Electro-Future

Where is my flying car?

After a recent code review, my co-workers and I had a rather animated discussion about the merits of relying on falsey-ness in code. Specifically, we were debating whether it was best-practice to rely on the fact that most languages treat certain values as false when evaluating the variable in a boolean context. I was against relying on this, prefering to check for the specific falsey value you expect, and pretty much everybody else was for it.

Since we’ve had this discussion before, and it’s difficult to illustrate all my points in person, I wanted to make my case on my blog.

A leaky abstraction

The prototypical falsey value is the integer 0, and it comes to us that way from our computers’ instruction sets. Computers don’t have boolean types, because they tend to operate on values stored in registers, which are, mostly, of a fixed size (32 or 64 bits, these days). I don’t know of any computers with a Jump on false or Jump on true instruction. Instead, they have things like Jump on zero, or Jump not zero.

Lower level langauges, like C, just take this idea and run with it. They don’t really provide many types that the hardware doesn’t natively understand, so they don’t provide a boolean type at all. The reason the C code:

int x = 0;

if( x ) {
    /* ... */
}

treats the integer x as a boolean is because the if statement is compling into a Jump if zero instruction (jumping past the body of the if).

This makes the language simpler, and the compiler easier to implement, but it forces the programmer to deal with a leaky abstraction. When you’re writing boolean logic, you’re thinking of values as only true or false, but the language is forcing you to consider the fact that the computer doesn’t actually understand those values at all, even though that might not be relevant to the task at hand.

Program in the language of your domain

There are two points where my coworkers suggested it was unreasonable for me to not want to rely on falsey-ness. The first is in arithmetic: specifically checking for divisibility using the modulo operator. Surely I must be OK with this construction, they said:

int x = 5;

if( x % 2 ) {
    printf("X is odd!\n");
}

The problem I have here is that, to paraphrase from the Pragmatic Programmer, you should be “programming in the language of the domain” (that is, in the language of the problem you’re trying to solve, not in the language of programming).

Your math teacher never told you “X is divisible by 2 if the remainder of X / 2 is false,” because that sentence doesn’t make any sense! In mathematics, integers aren’t true or false, so it would be devolving into programmer-speak to start considering certain numbers to be true or false. Explicitly checking for x % 2 != 0 is keeping the code in the language of its domain, which is mathematics.

This same concept also applies to the common practice of treating empty arrays as false. For example, JavaScript developers often write:

var things = [];

if (!things.length) {
    console.log("You didn't give me anything!");
}

Other languages, like Python, treat an empty array itself as being false:

things = []

if not things:
    print "You didn't give me anything!"

In both of these cases, the language of your domain is lists of things (possibly already lower level than you would ideally be writing), and it does not make sense to consider the length, or the array itself, as false. I don’t tell my wife “The grocery shopping went fast, there were false people in line at the checkout!” Similarly, if somebody were to ask you “What Faberge eggs do you have?” you would probably not answer “false.”

For things like strings and lists, I would actually rather have an empty function, which abstracts away the concept of a length of 0. This is especially useful when length isn’t as well defined: is the length of a Unicode string the number of characters, or bytes? Is the length of a node in a tree the number of direct children, or all children?

Code complexity

The next complaint leveled against me was that my code was “more complex” for being explicit about the condition being checked for. In particular, we were talking about if value is not None versus if value, and the argument was made that the explicit case was “more complex” because there was “more code.” I would argue, in fact, that there was not more code, and it is actually less complex to be explicit.

For example, consider the following two functions:

def noop():
    pass

def do_nothing():
    pass

I think it would be difficult to argue that the second version is “more code” just because the function name is longer. I think it is also impossible to argue that the second function is more complex than the first because the function name is longer.

In the case of the falsey comparison, I think it is actually considerably more complex than the explicit version, and here’s why:

if value is not None:
    print "We have something!"

if value is not None and \
        value != 0 and \
        value != [] and \
        value != {} and \
        value.__nonzero__():
    print "We have something!"

The second version is, roughly, what if value actually means in Python, and, written out that way, certainly looks a lot more complicated than the first if statement.

Principle of least surprise

One of the things that bothers me most about treating non-boolean values as falsey is that it violates the principle of least surprise.

When reading a piece of code, say a function, you have to operate on the assumption that the values have a fixed set of properties and behaviors, and that those properties and behaviors constitute a meaningful type of value. In a dynamically typed language, you (and the interpreter) use duck typing to define this type. That is, the type must contain all the properties accessed on the type by the code. In a statically typed language, the language requires the code to declare the type of the value and enumerate the exact properties and behaviors of that type.

In order to make the code more legible, it helps if, at least for the block being read, the type of the value represents some meaningful concept in the program. For example, an integer is a meaningful concept, understood by most people reading your code, and on which the standard arithmetic operators are well defined. If you are writing software for a human resources department, an Employee is meaningful type (whether you define it or not), and there are reasonable assumptions that can be made about the properties and behaviors of that type.

As I discussed earlier, having a boolean state usually doesn’t actually make sense for whatever conceptual type a value is supposed to have. When reading the HR code, I would expect an Employee to have a name, an ID, a mail stop, etc. I would not expect the employee to be false or true. When it comes to things like numbers and empty lists and nullable values, we’ve tricked ourselves into thinking that there are reasonable boolean representations of these types because computers, and programming languages, have provided them for so long. But, if we think about these types in terms of their real-world counterparts, we see that it really doesn’t make sense for many of them to exhibit boolean behavior.

When it makes sense

Of course, I do believe there are a few instances where a type can have a reasonable boolean representation. As an example, consider Python’s regular expression library: when you call re.match() you get a MatchObject back, or None if the string did not match the RE.

Consider, instead, a version which always returned a MatchObject, but which evaluated as False if there was no match. In that case, you could still write code like this:

if re.match(r"\w+", value):
    print "Value contains at least one word"

But, you could also write code like this:

def print_sub_groups(regexp, value):
    match = re.match(regexp, value)
    group = 1

    try:
        while True:
            print match.group(group)
            group += 1
    except IndexError:
        pass

In the specific domain of an object representing the result of matching a string against a regular expression, it actually does make sense for the match to be false, if there is no match. It also has the added benefit of allowing the programmer to not have to special-case whether the match was successful or not if it isn’t relevant to their code.

Avoiding sentinel values

Which leads to the title of this post, which is that treating values as falsey is often a code smell that you might be doing something else wrong.

Let’s consider a (very abridged) version of the code that initially started the discussion with my coworkers. The idea was that a function was returning a list of URLs which would ultimately be sent back to a client browser. In certain situations, in order to work around bugs in older OSes, we wanted to downgrade the connection from HTTPS to HTTP (when the initial request was already over HTTP). The code looked something like this:

def gather_urls(resource_ids, protocol_override=None):
    resources = db.get_resources(resource_ids)
    resource_urls = []

    for resource in resources:
        url = resource.get_url()

        if protocol_override:
            url = replace_protocol(protocol_override, url)

        resource_urls.append(url)

    return resource_urls

The line in contention was the if protocol_override, of course. The code calling this function determined if the protocol needed to be overridden, otherwise this function should return the pre-configured URLs for the resources.

The smell here is that this code has to be told whether or not to override the URLs at all, something it isn’t particularly concerned with (it was actually gathering more than just URLs from the resources). Instead of even needing to check whether the protocol needs to be overridden (the responsibility of other code), the code could be rewritten like this:

def identity_filter(url):
    return url

def force_protocol(protocol, url):
    return replace_protocol(protocol, url)

force_http = partial(force_protocol, 'http')


def gather_urls(resource_ids, url_filter=identity_filter):
    # ...
    url = url_filter(url)
    # ...

This code has a few advantages over the previous implementation: first, the decision of whether or not to override the protocol is completely up to the calling code, with no conditionals. Second, how you actually change the URL has been moved out of the function, so other URL filters can be constructed, or you could even chain filters together.

Hidden behind the falsey check in the first version was actually a sentinel value, None, which pointed to a larger problem with the implementation. Most falsey checks are really checks for a particular falsey value (or maybe a couple falsey values), which all send the code down a different code path.

Style

In the end, most of this comes down to personal preference. Except for a few corner cases, where the programmer doesn’t anticipate a particular falsey value being passed in, both styles of code are correct.

To me, the implicit check for falsey values reads like a number without a unit. The programmer is probably checking for a particular value, but just left it off as short-hand. I find it aggravatingly terse, and I have to stop and think what the programmer meant. If I’m debugging a problem with a particular input, I have to consider what the current value is, whether it would evaluate as false, and whether that is the correct behavior.

This is more difficult if you have to switch back-and-forth between languages often, because not all languages interpret the same values as falsey. For example, in Perl, "0" is falsey, but "0.0" is not (of course!). There is something to be said for taking full advantage of the language you’re using (instead of limiting yourself to the least common denominator of all languages), but relying on certain esoteric, and implicit, rules might make the code more difficult to maintain without any clear advantage.

Expressiveness is often used as an argument for allowing falseyness, and it’s certainly more expressive of a language to allow values to be falsey. However, there is a difference between expressiveness and clarity: just because a language allows you to express the phrase “if this list is false” does not mean that that is the clearest means of expressing what you actually mean (“if this list is empty,” or, better, “if there are no apples”).

comments powered by Disqus
Check me out