Case Statement Magic
Earlier this week, a conversation on the Seattle.rb Slack reminded me of one of my favorite hidden gems in Ruby, the case
statement. Most languages have syntax for multiway branching. Java calls it switch
. Lisp uses cond
. Ruby calls it case
which relates back to the mathematical roots of this construct. Ruby’s implementation of this feature is particularly flexible, and many folks don’t realize all the things you can do with it.
Testing Values
Here’s a straightforward example of a case statement the way we typically think of them.
Ruby evaluates the variable my_var
against each of the conditions listed. If the condition is true, the associated code branch is executed. But you can put more than a single value after the when. In this example, I use several different values separated by commas to specify each branch.
And in this example, I use ranges instead of explicit values. Once a matching “when” is found that branch is executed and the rest are skipped over. There is no “fall-through”.
You can also use regular expressions.
Adding More Flexibility
The above shouldn’t look too bizarre to folks who know Java or C. The syntax is a bit different, but the basic use case of testing a variable against several possible values is the same. This isn’t how I normally use a case statement in Ruby though. Most of my case statements look something like this:
In Ruby the expression after the reserved word case
is optional. When used like this the case statement is just a different way of doing multiple if/elsif/else lines. It is also analogous to cond
in Lisp, which works the same way. I prefer to use case over if/elsif/else. I like that the whens and conditions line up vertically. In my brain, case means picking among more than two alternatives, and so semantically case makes more sense when I have a multiway conditional. Using case for multiway conditionals is so habitual for me that I have to think for a second about how Ruby spells “else if” if I’m using “if” for branching.
Our Friend ===
No discussion of case would be complete without discussing ===
, often called “case equality”. ===
is also called the “is_a?” operator. mammal === pangolin
would be true because a pangolin is a mammal. Note that the order is backwards of what we’d normally think in Ruby, it isn’t mammal.is_a? pangolin
, but pangolin.is_a? mammal
. The case statement uses ===
when determining which branch to follow and this allows for statements like this:
But ===
can be overridden by any class and some classes do interesting things. Range defines ===
as an alias for include?
. Regexp defines ===
as an alias for match
. While these don’t make sense with the ‘is_a?’ explanation they do make sense if you think in terms of sets. ===
is set inclusion. For example a === b
is true when b is in the set defined by a. In that context Range#===
being defined as include?
makes sense. when (1..5)
is testing whether the variable is part of the set of things defined by (1..5)
.
But there are some even more unusual definitions of ===
in the Ruby Standard Lib. In Brandon Weaver’s Triple Equals Black Magic I learned that Proc defines ===
to be call and IPAddr
defines it to be subnet inclusion. This means we can do things like this:
I probably wouldn’t use this in production code, just like I won’t call next
with a return value in production code. While both of these are valid Ruby, they aren’t idiomatic. Code that isn’t idiomatic is harder to understand and therefore maintain. So I’ll continue marveling at how awesome Ruby is but I’ll avoid using Procs in case statements unless the alternative is even less readable.