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!
That was a really interesting episode!
Interesting article. I couldn’t tell from reading the article what the difference in behaviour was.
Do you mean the following?
TrueClass XOR doc:
FalseClass XOR doc:
… or something else?
Good to know, thanks for the episode, Avdi! 👍
Sigh This is the same stuff I’ve heard before. By this reasoning, all numbers should be given a different class. It would require an infinite number of classes, but that’s not the point. It might be convenient in some cases that having a separate class, but doesn’t say that they cannot be derived from the same class representing the concept they are trying to convey, that of a Boolean.
“by this reasoning” are you saying that no integers share numeric methods having the same implementation? Remember, as a dynamic language, in Ruby classes are about sharing implementation, not about abstract representation of concepts!
— original message —
TIMTOWDY
if [true,false].include?(value)
# …
end