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"
end
# 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"
end
# 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!"
end
# 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!"
end
# 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
end
end
class Empty
def self.===(item)
item.response_size == 0
end
end
case http_response
when Empty
puts "response was empty"
when Success
puts "response was a success"
end