Ruby's case statement - advanced techniques

Nothing could be simpler and more boring than the case statement. It's a holdover from C. You use it to replace a bunch of ifs. Case closed. Or is it?

Actually, case statements in Ruby are a lot richer and more complex than you might imagine. Let's take a look at just one example:

case "Hi there"
when String
  puts "case statements match class"

# outputs: "case statements match class"

This example shows that case statements not only match an item's value but also its class. This is possible because under the hood, Ruby uses the === operator, aka. the three equals operator.

A quick tour of the === operator

When you write x === y y in Ruby, you're asking "does y belong in the group represented by x?" This is a very general statement. The specifics vary, depending on the kind of group you're working with.

# Here, the Class.===(item) method is called, which returns true if item is an instance of the class 

String === "hello" # true
String === 1 # false

Strings, regular expressions and ranges all define their own ===(item) methods, which behave more or less like you'd expect.  You can even add a triple equals method to your own classes.

Now that we know this, we can do all sorts of tricks with case.

Matching ranges in case statements

You can use ranges in case statements thanks to the fact that range === n simply returns the value of range.include?(n). How can I be so sure? It's in the docs.

case 5
when (1..10)
  puts "case statements match inclusion in a range"

# outputs "case statements match inclusion in a range"

Matching regular expressions with case statements

Using regexes in  case statements is also possible, because /regexp/ === "string" returns true only if the string matches the regular expression. The docs for Regexp explain this.

case "FOOBAR"
when /BAR$/
  puts "they can match regular expressions!"

# outputs "they can match regular expressions!"

Matching procs and lambdas

This is kind of a weird one. When you use   Proc#===(item), it's the same as doing Proc#call(item). Here are the docs for it. What this means is that you can use lambdas and procs in your case statement as dynamic matchers.

case 40
when -> (n) { n.to_s == "40" }
  puts "lambdas!"

# outputs "lambdas"

Writing your own matcher classes

As I mentioned above, adding custom case behavior to your classes is as simple as defining your own === method. One use for this might be to pull out complex conditional logic into multiple small classes. I've sketched out how that might work in the example below:

class Success
  def self.===(item)
    item.status >= 200 && item.status < 300

class Empty
  def self.===(item)
    item.response_size == 0

case http_response
when Empty
  puts "response was empty"
when Success
  puts "response was a success"