The case of case equality operator

Ruby is a nice and easy to read language. There are, however, some quirks which might surprise you. Some of them are easy to deal with when you start learning it, because they come up often enough. Certain keywords fall into this category, probably the most common is elsif not being elseif or else if. A little less common is the switch statement. Most languages have switch as a keyword for the whole decision block and case for each branch, default for the default catch-all case. Ruby uses case, when, and else respectively.

Some of the strangeness comes from function names in the standard library. Personally, I often struggle with names of String functions. String#replace replaces the whole string, not a matching substring (what’s that even for?). String#tr replaces each character from its first argument by the corresponding character from the second argument, not a matching substring by the second argument. For that, you need String#sub (or String#sub!) and possibly String#gsub (or String#gsub!) to replace all occurrences. Of course, sub stands for substitute and it makes sense, but so does replace. Yet another example of strangely named function is String#strip. Yes, it strips the string of leading and trailing whitespace, but I haven’t yet encountered any other language which doesn’t call it trim.

These are all only cosmetic details and you get used to them as you write more Ruby code. There is, however, a construction which seems crazy when you look at it:

class Cat
end

class Dog
end

foo = Cat.new

case foo.class
when Cat
  puts 'Meeow'
when Dog
  puts 'Wuff, wuff'
else
  puts 'What?'
end
# => What?

How is it, that a comparison of foo’s class which is clearly an instance of the Cat class doesn’t get matched? What has gone wrong with the world, especially when you try:

if foo.class == Cat
  puts 'Meeow'
elsif foo.class == Dog
  puts 'Wuff, wuff'
else
  puts 'What?'
end
# => Meeow

The devil is hidden in the case equality operator ===. It is a method defined in the Object class, i.e. the base class of everything in Ruby, just for case comparison. This method gets internally called when you write a case statement and by default it compares the class attribute of the objects on either side. Therefore, if you pass foo.class the argument used is the Cat class object (not foo, an instance of that class) and Object#class is called on it. Class of Cat is Class; therefore, it falls through to the else branch.

What you need to do, is leave out the call to Object#class in the case statement and everything will be fine.

case foo
when Cat
  puts 'Meeow'
when Dog
  puts 'Wuff, wuff'
else
  puts 'What?'
end
# => Meeow

There’s a great answer over at StackOverflow by Jörg W Mittag who suggests to think about the case equality operator rather as a set subsumption test operator (a === b is true when b can be considered a representative of a) and demonstrates how this mental model helps to understand less common usage. Luckily, these fancy cases are rarely seen in the wild except for books or tutorials about Ruby. Then you look at them and think “huh, that’s neat” and forget about it two pages later.

We're looking for developers to help us save energy

If you're interested in what we do and you would like to help us save energy, drop us a line at jobs@enectiva.cz.

comments powered by Disqus