A clever Ruby equality trick
April 22, 2012 at 6PM
Kyle Cronin

Consider the following Ruby class:

class Site
  def initialize(domain)
    @domain = domain
  end
end

A simple class, Site, that is initialized with a value, domain, that is then stored as an instance variable. Suppose we now want to add equality testing. Specifically, we want to establish that two Sites with the same domain are equal. In a language like Java, in the class definition we have access to all the private instance variables, so the solution would look something like:

public boolean equals(Site other)
{
    return domain.equals(other.domain);
}

However, in Ruby, instance methods do not have such access. Private data remains private even among other instances, so the following would not work:

def ==(other)
  @domain == other.domain
end

So how should we go about solving this problem? One approach would be to add domain as a reader on Site:

attr_reader :domain

This would add the necessary method to Site to enable the == method we wrote earlier to work. However, the downside to this is that it makes the private variable public. Let’s assume for this example that we want to keep the domain variable private for some reason. What other alternatives are there?

One solution would be to provide a method that takes the domain as an input and compare that with the locally stored domain:

def domain_equal(domain)
  @domain == domain
end

Our == method can then be constructed using this method:

def ==(other)
  other.domain_equal(@domain)
end

This is better - the value of the domain is no longer directly available, and although passing an object into == that responds to domain_equal would still expose it, this level of protection is sufficient for our purposes.

However, we can still get a bit more clever. Instead of having two methods, we could combine the functionality of domain_equal into == by testing for the class of other. If it’s the same class as domain, we’ve passed in a domain instead of another Site object, so we perform the operation in domain_equal. Otherwise, call the operation as if it were domain_equal:

def ==(other)
  if @domain.class == other.class
    @domain == other
  else
    other.==(@domain)
  end
end

Not bad! Note that in Ruby, other.==(@domain) can be rewritten as other == @domain. Also note that because @domain.class and other.class are equal, and because equality is commutative1 (i.e. a == b is the same as b == a) we can swap the order of @domain and other:

def ==(other)
  if @domain.class == other.class
    other == @domain
  else
    other == @domain
  end
end

Now notice that the bodies of both the if and the else are identical. Therefore, we can remove the test and write == like so:

def ==(other)
  other == @domain
end

Pretty amazing!

This method is deceptively simple - let’s step through how it works. I’m going to use the notation of X.domain to indicate the private variable domain on X:

a = Site.new("google.com")
b = Site.new("google.com")

a == b
 ↳ b == a.domain
    ↳ a.domain == b.domain

When a == b is called, this in turn calls b == a.domain, which in turn calls a.domain == b.domain and performs the desired equality comparison. However, while this trick is clever, it probably should be avoided in actual code because how the method operates may not be obvious to the casual reader.


1 Actually, this whole trick works because equality in Ruby is not commutative - it’s just an instance method on the left operand. However, we can make the switch from other.==(@domain) to other == @domain because the if tests to make sure that the classes of both @domain and other are equal. Since they are, and since we are assuming that equality on instances of those classes is commutative, we can make this swap. Interestingly, even if == is not commutative on the domain class, the operation still preserves the order, meaning that a == b if a.domain == b.domain and b == a if b.domain == a.domain.

Article originally appeared on Kyle Cronin (http://kylecronin.me/).
See website for complete article licensing information.