Boolean values represent truth and falsehood. Ruby has true
and false
values, but if you’re used to languages where both of those values are instances of a Boolean
class, you’re in for a surprise in Ruby. Ruby has no Boolean class!
If Ruby is your first programming language, you might be wondering what the fuss is about. On the other hand if you’re coming to Ruby from another language, you’re not the first person to be confused by this omission! Either way, understanding the logic behind this design choice (and yes, there is a reason!) will help you understand both Ruby booleans and Ruby’s object-oriented design philosophy more thoroughly.
What do you think? Does it make sense for Ruby to go without a 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.
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.
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.
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.
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
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!
Responses