The personal webblog of Tamal White

===

It is said that Ruby is designed to make programmers happy. Everything is an object in Ruby, and almost every part of the language is simply a method call on an object:

1 + 1 # => 2
1 == 1 # => true
# can be written as
1.+(1) # => 2
1.==(1) # => true

Ruby’s syntatic sugar allows us to not have to explicitly call those methods using dot notation, but we can if we want to. The common rule of thumb is to write your code in a way that makes most sense to someone reading it. If it makes sense, it is beautiful. If it is beautiful, it makes us happy.

Ruby’s case statement operator implementation is one of my favorite examples of how Ruby code can be beautiful. A typical case statement looks like this:

case n
when 1
  # do something
when 2
  # do something
else
  # do something else
end

One may assume (as it is in many other languages) to be exactly the same as this:

if n == 1
  # do something
elsif n == 2
  # do something
else
  # do something else
end

However the equivalent in Ruby is actually this:

if 1 === n
  # do something
elsif 2 === n
  # do something
else
  # do something else
end

The === (“threequals” or case-equality) operator is defined in Object as being effectively ==, but because it is really just a method it can be overridden in subclasses.

Many Ruby core classes have already overridden this method, allowing beautiful and expressive case statements:

Class returns true if the argument object is an instance or descendant of the class.

string = "My dog has fleas."
number = 42

String === string # => true
Numeric === number # => true

case foo
when String
  # do something with a string
when Numeric
  # do something with a number
end

Regexp returns true if the argument object matches the regular expression.

string = "Cats can shoot lazers from their eyes!"

/dog/ === string # => false
/cat/ === string # => false
/cat/i === string # => true

case foo
when /\.txt$/
  # do something with a text file
when /\.rb$/
  # do something with a ruby file
end

Range returns true if the argument object is within the range.

number = 42

(0..10) === number # => false
(0..42) === number # => true
(0...42) === number # => false

case foo
when 0...10
  # Do something with 0-9
when 10...20
  # Do something with 10-19
end

You can define it in your own classes. Here is a contrived example:

class Grade
  attr_reader :grade
  
  def initialize(grade)
    @grade = grade
  end
  
  def ===(obj)
    if obj.respond_to?(:grade)
      grade == obj.grade
    else
      grade == obj
    end
  end
end

class Student
  attr_reader :name, :grade
  
  def initialize(options)
    @name = options[:name]
    @grade = options[:grade]
  end
  
end

freshman = Grade.new(9)
sophmore = Grade.new(10)
junior = Grade.new(11)
senior = Grade.new(12)

foo = Student.new(name: "Jim", grade: 10)

case foo
when freshman
  "#{foo.name} is a noob!"
when sophmore
  "#{foo.name} has hit the double digits."
when junior
  "#{foo.name} is chugging along."
when senior
  "#{foo.name}'s last year!"
else
  "#{foo.name} isn't even in high school"
end

Like in the above example, case statements can be a code smell, they can be an indication that there is a better way to do what you’re trying to accomplish. However, when they are the right solution, understanding how Ruby implements them can help us write succinct, beautiful code.