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

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

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!