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.

1
2
3
4
5
6
7
8
case my_var
when 1
  puts "one"
when 2
  puts "two"
else
  puts "too big!"
end

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.

1
2
3
4
5
6
7
8
case my_var
when 1, 3, 5
  puts "odd"
when 2, 4, 6
  puts "even"
else
  puts "uh-oh"
end

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”.

1
2
3
4
5
6
case my_var
when (1..5)
  puts "Left Hand"
when (6..10)
  puts "Right Hand"
end

You can also use regular expressions.

1
2
3
4
5
6
case my_string
when /[a-z]+/
  puts "lower"
when /[A-Z]+/
  puts "UPPER"
end

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:

1
2
3
4
5
6
7
8
case 
when x % 3 == 0
  "Multiple of 3"
when y.odd?
  "Y is Odd"
when z.even?
  "Z is Even"
end

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:

1
2
3
4
5
6
7
8
case my_var
when Numeric
  puts "numbers"
when String
  puts "String"
when Regexp
  puts "Reg Ex"
end

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:

1
2
3
4
5
6
case my_var
when Proc.new { |n| n.odd? }
  puts "Odd number"
when Proc.new { |n| n.even? }
  puts "Even number"
end

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.