Episode #125: And/Or

Upgrade to download episode video.

Episode Script

Ruby has two different forms of logical operators. It has the “symbolic” form, such as && and ||. And it has English-style operators and and or.

nil || 42                       # => 42
42 && 23                        # => 23

nil or 42                       # => 42
42 and 23                       # => 23

At first glance the English operators and the symbolic operators seem to synonyms for each other. As a result, we may be tempted to use the English versions in preference to the symbolic versions in the interests of readability.

However, it doesn't take long before we start seeing surprising differences in behavior between the two sets of operators. For instance, here's some code that assigns a user object's name to a user_name variable. It uses the && operator to ensure that the .name message is only sent if the user is non-nil. So long as the user exists, the variable winds up having the expected username value.

But if we replace the && with the English and operator, the user_name variable takes on the value of the entire User object instead.

user = Struct.new(:name).new("Avdi")
user_name = user && user.name
user_name                       # => "Avdi"
user_name = user and user.name
user_name                       # => #<struct name="Avdi">

This happens because the English and has much lower operator precedence than the &&. So While the && operator takes precedence over the assignment, when it is replaced with the English and the assignment happens first.

user = Struct.new(:name).new("Avdi")
name = (user && user.name)
name                            # => "Avdi"
(name = user) and user.name
name                            # => #<struct name="Avdi">

Even more surprisingly, differences show up even in the simplest of logical expressions. Here's an expression using only symbolic operators. When we change all of the operators to English versions, the result changes.

😡 || :y && nil                 # => 😡
😡 or :y and nil                # => nil

The reason for this is that not only do the English operators have lower precedence than the symbolic ones, but they also have differing precedence in relation to each other. Specifically, while the symbolic && has higher precedence than ||, the English and and or operators have equal precedence, so they are evaluated left to right.

😡 || (:y && nil)               # => 😡
(:x or :y) and nil              # => nil

At this point you may be starting to feel like the English logical operators are a trap: they look attractive, but when we actually put them to use they have surprising, seemingly arbitrary semantics.

To understand why these operators are the way they are, we have to understand where they come from. The English versions of the logical operators are one of the many features Ruby inherits from Perl. And in Perl, these operators have a purpose that's distinct from the symbolic logical operators: they are used as control-flow operators.

For example, here's a line of idiomatic Perl code straight from the Perl documentation. It tries to change working directory. If the operation fails, it raises an error, using the die keyword. In this example the program isn't interested in the boolean result of the or operator. Rather, it is exploiting the short-circuiting behavior of or to only execute the code on the right if the code on the left returns a falsey value.

chdir '/usr/spool/news' or die "Can't cd to spool: $!\n"

Here's some Ruby code which behaves similarly. It uses the unless statement modifier to raise an exception unless it is able to read a line of text from $stdin.

raise "Can't read from STDIN" unless line = $stdin.gets

Let's say we'd like to put the read first and the error handling after it, similar to the Perl code we saw a moment ago. In order to do so, we might try to use logical a logical OR operator like so. But when we try to run it, we get a syntax error. As a result of the operator precedence rules, Ruby gets confused by this code.

line = $stdin.gets || raise "Can't read from STDIN" # !> possibly useless use of a literal in void context
# ~> -:1: syntax error, unexpected tSTRING_BEG, expecting keyword_do or '{' or '('
# ~> line = $stdin.gets || raise "Can't read from STDIN"
# ~>                              ^

I could diagram exactly why this parse failed, but it's not worth it because we can easily fix the problem by switching from symbolic || to English or.

line = $stdin.gets or raise "Can't read from STDIN"
# ~> -:1:in `<main>': Can't read from STDIN (RuntimeError)

Let's move on to the and operator. Here's some Ruby code which outputs a notification if an enable_notifications flag is true. Right now it's using an if statement modifier to control whether or not the notification is sent.

def notify(message)
  puts "*** #{message}"
end

enable_notifications = true

notify "Something happened!" if enable_notifications
# >> *** Something happened!

Let's say we wanted to switch to using a logical operator to make the decision. We switch things around to put the flag first and use symbolic && to only send the notification when the flag is true. But then we run into a problem.

def notify(message)
  puts "*** #{message}"
end

enable_notifications = true

enable_notifications && notify "Something happened!" # !> possibly useless use of a literal in void context
# ~> -:7: syntax error, unexpected tSTRING_BEG, expecting keyword_do or '{' or '('
# ~> enable_notifications && notify "Something happened!"
# ~>                               ^

Once again, we get a syntax error, because the Ruby gets confused about how the && fits together with the call to notify without parentheses. We could change the code to introduce parentheses… or we could simply use the English and operator and have everything just work.

def notify(message)
  puts "*** #{message}"
end

enable_notifications = true

enable_notifications and notify "Something happened!"
# >> *** Something happened!

In these two examples we can begin to see the reasoning behind the low precedence of these operators, and how it can be advantageous when they are used for control flow. Although truth be told, I don't really like the way this new and code reads; I preferred the original version with an if modifier on the end of the line. Let's move on to another example where English logical and makes more sense.

Here's some code that parses chunked HTTP content, as a web server might deliver it when it is streaming out data on the fly. The chunked encoding is a simple format where each chunk of data is preceded by a line containing a number indicating the length of the next chunk.

This code is written defensively to end early if any of its expectations about the incoming data are not met. First it reads a header line, which should contain the size of the next chunk. If that succeeds, it uses that size information to read the next chunk. And if that succeeds, it appends the last read chunk to an accumulator string.

require 'stringio'
chunked_data = StringIO.new("7\r\nHello, \r\n6\r\nworld!\r\n0\r\n")
data = ""
until chunked_data.eof?
  if chunk_head = chunked_data.gets("\r\n")
    if chunk = chunked_data.read(chunk_head.to_i)
      data << chunk
    end
  end
end
data                            # => "Hello, world!"

We can see this code forming a “chain” of sorts: We have a series of operations to perform, but if any of them is unsuccessful we don't want to proceed any further. We might think to turn this into a visual chain by stringing the statements together with AND operators instead of nesting them in if statements. We try this using symbolic &&, but even with parentheses around all method arguments we run into trouble.

require 'stringio'
chunked_data = StringIO.new("7\r\nHello, \r\n6\r\nworld!\r\n0\r\n")
data = ""
until chunked_data.eof?
  chunk_head = chunked_data.gets("\r\n") &&
    chunk = chunked_data.read(chunk_head.to_i) &&
    data << chunk
end
data                            # => 
# ~> -:7:in `<main>': can't convert nil into String (TypeError)

You know what comes next: when we replace the symbolic && operator with English and, our problems go away. We've successfully built a chain of short-circuiting dependent operations

require 'stringio'
chunked_data = StringIO.new("7\r\nHello, \r\n6\r\nworld!\r\n0\r\n")
data = ""
until chunked_data.eof?
  chunk_head = chunked_data.gets("\r\n") and
    chunk = chunked_data.read(chunk_head.to_i) and
    data << chunk
end
data                            # => "Hello, world!"

So what can we learn from these examples? First, we've seen that the English logical operators behave in surprising ways when used in boolean calculations. Second, we've seen that those very surprising parsing rules make these operators better suited than their symbolic counterparts when used as control-flow operators.

My advice is to take a page from Perl, and use the English logical operators strictly as control-flow constructs rather than evaluating them for their return value. Much as it might seem like an aesthetic improvement to substitute the English versions for symbolic operators in logical expressions, they simply aren't as well suited for that role.

That's it for now. Happy hacking!