Why Ruby doesn’t have a Boolean class

One peculiarity of Ruby that trips up a lot of programmers migrating from other languages is the fact that while it has true and false types, they don’t share a common Boolean class. Why is this? In this RubyTapas freebie episode, you’ll see how, far from being an arbitrary quirk, this design choice flows directly out of Ruby’s nature as a dynamic language.

(This post was adapted from Episode #093: Boolean)

Wait, where’s the Boolean class??

In some programming languages, true or false values are represented with a special data type called a Boolean. Others make do by using certain values of other data types to represent true and false. For instance, the integer 0 for “false” and the integer 1 for “true”.

Ruby is a bit of an oddball in that while it has explicit values to represent true and false, there is no Boolean data type. Instead, in Ruby truth is represented by the sole instance of TrueClass, and falsehood is represented by the sole instance of FalseClass. TrueClass and FalseClass are both direct descendants of the Object class—they share no common Boolean parent class.

true.class                      # => TrueClass
false.class                     # => FalseClass
TrueClass.ancestors             # => [TrueClass, Object, Kernel, BasicObject]
FalseClass.ancestors            # => [FalseClass, Object, Kernel, BasicObject]

This is often surprising to programmers coming from other languages, and questions about why this is come up regularly on the Ruby-Talk mailing list.

Reason #1: dynamic typing

To understand why Ruby has no Boolean class, we need to start with Ruby’s nature as a dynamically-typed language. Unlike statically-typed languages such as Java, C++, or Haskell, in Ruby we don’t have to declare the type of an argument or a variable before assigning a variable to it. So for instance in this method, the only thing indicating that the single argument is expected to be a true or false value is the name. There is no need to declare a type.

def want_fries_with_that?(true_or_false)
  puts "Customer wants fries: #{true_or_false}"
end

want_fries_with_that?(false)    
# >> Customer wants fries: false

This eliminates one reason for true and false to share a parent class: since any type of value can be assigned to any variable, we don’t need a Boolean type in order to declare that a variable may have either a true or false value.

Reason #2: shared behavior (or the lack thereof)

The other common reason for two objects to share either a class or a superclass is if they share common behavior. To see if this reason applies to true and false, let’s take a look to see if they share any methods in common.

First, let’s see how many methods true and false respond to.

true.methods.count              # => 59
false.methods.count             # => 59

In typical Ruby form, the answer is “quite a few”. Now let’s use the Array intersection operator to see how many methods true and false share in common.

(true.methods & false.methods).count # => 59

The number is the same, so it’s starting to look like these objects could potentially share a lot of behavior. Now let’s subtract out from these shared methods the methods that they both inherit from the base Object class.

((true.methods & false.methods) - Object.instance_methods).count
# => 3

Here’s where things get interesting: out of all their common methods, only three are unique to true and false objects. The rest are common across all objects.

What do true and false have in common?

Let’s take a closer look at these three methods. Examining the list, we see that they are three non-lazy boolean operators: logical AND, represented by the single ampersand; logical OR, represented by the single pipe symbol; and logical XOR, represented by the caret.

((true.methods & false.methods) - Object.instance_methods)
# => [:&, :|, :^]

When we experiment with these operators, we find that they each have different results depending on whether the value on the left is true or false.

true | false                    # => true
false | false                   # => false

true & true                     # => true
false & true                    # => false

true ^ true                     # => false
false ^ true                    # => true

While these methods share names, the true and false versions have differing behavior. So in fact, there is no common boolean behavior between true and false that could be moved up into a common Boolean class. true and false don’t share a class because, quite simply, they don’t share anything in common except for things common to all objects.

Reason #3: type-matching convenience

Another reason we might wish that true and false would share a type is when matching values based on type. For instance, here’s a method that processes responses to a one-question, yes-or-no survey. Each response can be one of four possible types:

  • true, meaning yes;
  • false, meaning no;
  • nil, meaning “Not Applicable”; and
  • a String, meaning that the respondent chose to fill in some response
    other than yes or know.
responses = [
  true,
  true,
  nil,
  false,
  "I reject the premise!",
  true
]

def process_response(response)
  # ...
end

@votes = {true => 0, false => 0}
def tally_vote(response)
  @votes[response] += 1
end

responses.each do |response|
  process_response(response)
end
puts @votes.inspect

Let’s build a case statement to switch on the type of the value. For “other” responses, we can match a String class. For “N/A” responses, we can match nil. For true or false, we’d like to delegate to a separate method which tallies up the votes.

def process_response(response)
  case response
  when String
    puts "Other: #{response}"
  when nil
    puts "N/A"
  when Boolean
    tally_vote(response)
  end
end

A case for a Boolean type

In this case, it would be convenient to match a single Boolean class. However, since Ruby allows us to match multiple criteria in a single case branch, it’s no great hardship to list both true and false together.

def process_response(response)
  case response
  when String
    puts "Other: #{response}"
  when nil
    puts "N/A"
  when true, false
    tally_vote(response)
  end
end

It’s only in if statements that testing for a boolean value gets seriously ugly.

if value == true || value == false
  # ...
end

For this reason, when writing code that needs to explicitly differentiate true or false from other types, I prefer to use case statements over if/else statements.

OK, that’s it for today. Happy hacking!