In Progress
Unit 1, Lesson 1
In Progress

Identity And Equality

Video transcript & code

We have been slowly building up a concept of a class that represents a quantity. So far, we have built a Feet class which has a magnitude, and which is immutable like Ruby's core numeric types.

class Feet
  attr_reader :magnitude

  def initialize(magnitude)
    @magnitude = magnitude.to_f
    freeze
  end

  def to_s
    "#{@magnitude} feet"
  end

  def +(other)
    Feet.new(@magnitude + other.magnitude)
  end
end

Today let's talk about identity and equality, and how these concepts apply to our Feet class.

Let's instantiate three Feet objects. We'll call the first one, because it represents a measurement of one foot. We'll call the second two, and give it a magnitude of 2. Finally, we'll make an object called other_one. As suggested by its name, this object has a magnitude of one, just like our first instance. But these are two separate and distinct objects, as we can confirm by comparing their object IDs.

require "./feet"

one = Feet.new(1)
two = Feet.new(2)
other_one = Feet.new(1)

one.object_id                   # => 13510700
other_one.object_id             # => 13510660

Ruby has a whopping four different standard methods for testing if two objects are equal to each other, and each method has its own special purpose. Today we'll start with the most common method: the equivalence operator, or double-equals. Let's see how our three objects compare to each other in terms of equivalency.

one equals itself, which is not too surprising. one doesn't equal two, which is also predictable. But when we compare one to other_one, we get false, which might not be what we expected.

require "./feet"

one = Feet.new(1)
two = Feet.new(2)
other_one = Feet.new(1)

one == one                      # => true
one == two                      # => false
one == other_one                # => false

The reason for this false result is that by default, Ruby defines equality in terms of object identity. That is, out of the box, an object is equal to itself and no other object in the system.

This actually turns out to be a pretty sensible default. For instance, consider a class representing users, that has only a name attribute. There might be two users in the system named "John Smith". But that doesn't make them the same person. Having them compare as equal just because they happen to share a name would be surprising and could potentially cause problems.

class User
  def initialize(name)
    @name = name
  end
end

u1 = User.new("John Smith")
u2 = User.new("John Smith")

u1 == u2                        # => false

With our Feet class it's a different story, however. We want objects representing quantities to base equality on their state, rather than their identity. And we want this for more than just the purpose of explicit comparisons. Consider the case of checking to see if a given value exists in an array. When we ask an array that includes a one-foot value if it includes one foot, it says no. This is probably not what we expect.

require "./feet"

[Feet.new(1)].include?(Feet.new(1)) # => false

To bring things in line with our expectations, we modify the Feet class to override the double-equals equivalence operator.

Our custom operator first checks that the other object is also a Feet object, because at least for now that's the only kind of object Feet knows how to compare itself to. Then it checks its own magnitude against the other object's magnitude.

class Feet
  attr_reader :magnitude

  def initialize(magnitude)
    @magnitude = magnitude.to_f
    freeze
  end

  def to_s
    "#{@magnitude} feet"
  end

  def +(other)
    Feet.new(@magnitude + other.magnitude)
  end

  def ==(other)
    other.is_a?(Feet) && magnitude == other.magnitude
  end
end

I want to briefly emphasize the importance of first checking for the class of the other object in equality comparisons. If we don't include that detail, then it is possible for a comparison to raise NoMethodError when it tries to send the #magnitude message. As a general rule, it is rude for a simple comparison of two objects to raise an exception.

When we reevaluate, we can see that the situation has improved. one is now equal to other_one, and the array now reports that it does include a quantity of one foot.

require "./feet2"

one = Feet.new(1)
two = Feet.new(2)
other_one = Feet.new(1)

one == one                      # => true
one == two                      # => false
one == other_one                # => true

[Feet.new(1)].include?(Feet.new(1)) # => true

What if for some reason we still want to check if two variables refer to the exact same object in memory, rather than just having equal values? That is what the equal? predicate is for. When we test for equality this way, we can see that the answers are exactly the same as our initial results with an unmodified equivalence. The object referred to by the variable one is the same as itself, but it is not identical to any other object, not even the object referred to by other_one.

require "./feet2"

one = Feet.new(1)
two = Feet.new(2)
other_one = Feet.new(1)

one.equal?(one)                      # => true
one.equal?(two)                      # => false
one.equal?(other_one)                # => false

There is one other type of equality we'll talk about before we wrap up. This is case-equality, determined with the "threequals" operator.

Case-equality is what powers Ruby's extraordinarily flexible case statements. When we add case equality to our list of experiments, we can see that it mirrors the results of the equivalence operator. There is a reason for this: by default on a raw object, the threequals operator just delegates to the equivalence operator.

require "./feet2"

one = Feet.new(1)
two = Feet.new(2)
other_one = Feet.new(1)

one === one                      # => true
one === two                      # => false
one === other_one                # => true

In our case this is exactly what we want. When we pass a feet object to a case statement, it matches the expected branch.

require "./feet2"

one = Feet.new(1)

case one
when Feet.new(1) then puts "one"
when Feet.new(2) then puts "two"
end
# >> one

There is one other kind of equality which we haven't touched on yet: hash equality. But we'll save that for an upcoming episode. Happy hacking!

Responses