Sunday
Apr222012

A clever Ruby equality trick

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.

PrintView Printer Friendly Version

EmailEmail Article to Friend

References (14)

References allow you to track sources for this article, as well as articles that were written in response to this article.

Reader Comments (4)

Have you come up to this method yourself? Personally I wouldn't manage to create such a trick...

Yes, I came up with it myself. In fact, the thought process in the article is pretty much the thought process I used when coming up with it, just with a bit more exposition at the beginning to set up the problem. I will admit, I didn't intend to come up with this, and if I were given the task to do so (without knowing what the solution was) I probably couldn't. However, it's something I did stumble across and I thought it would be great to document.

May 8, 2012 | Registered CommenterKyle Cronin

This is amazing. I'm wondering if it would still work through a decorator chain. And how about with testing against multiple instance variables. I'll update with results.

Regarding not using it in code that other people are going to read, I think it's fine as long as you have tests covering it; then it people want to know what it does they can read the tests, and also be amazed by it's beautiful simplicity.

August 13, 2014 | Unregistered CommenterCameron

You’ve got some interesting points in this article. I would have never considered any of these if I didn’t come across this. Thanks!

January 2, 2015 | Unregistered Commenterverhuisbedrijf hilversum

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>
« I'm moving to the Boston area | Main | Sparrow for iPhone review »