nuclearsandwich's

Mad scribblings and inexpert musing

A Look at Ruby Case Expressions

published: 8 Sep 2011 11:09:00AM

Ruby, like many languages has a case-when construct for more refined conditional execution than if-then-else can provide. Technically all case constructs could be written as if-then-else-if-then...else but you wouldn't enjoy it.

Ruby's case-when is an expression

However, there are a number of things that make Ruby's case-when special. The first comes from Ruby's functional roots. It's powerful but deceptively simple. Ruby's case-when construct is an expression. Which means, among other things, it returns a value.

In a traditional imperative style a case statement might look like:

conditional_variable = :some_default_value
case our_condition
when :first
  conditional_variable = :one_thing
when :then
  conditional_variable = :another
end

The that isn't ideal for a number of reasons. conditional_variable is set visibly three times which as any freshman can tell you is frowned upon. Academics have stuffier reasons for it but the most important is that it decreases readability because we aren't sure what the value will be after execution. Setting it so many times is also a code duplication, albeit a small one. We can take advantage of Ruby's case-when expression with this code:

conditional_variable = case our_condition
                       when :first
                         :one_thing
                       when :then
                         :another
                       else
                         :some_default_value
                       end

Doesn't that look nicer? conditional_variable is only set once and the intent is clear, we want the value to depend on our_condition.

case-when uses triple equals for comparison

This got me good this morning and spawned this article. If you're not familiar with === in Ruby then the best explanation comes from _why's Poignant Guide to Ruby which really is worth reading. I've referred back to it multiple times to help myself and those around me fully understand basic Ruby concepts like nil, truthiness and falsiness, and, fanfare please, double equals versus triple equals.

The double equals gives the appearance of a short link of ropes, right along the sides of a red carpet where only true can be admitted.

if approaching_guy == true
  puts "That necklace is classic"
end

... [This case-when statement]

case year
when 1894
  "Born."
when (1895..1913)
  "Childhood in Lousville, Winston Co., Mississippi."
else
  "No information about this year."
end

is identical to

if 1894 === year
  "Born."
elsif (1895..1913) === year
  "Childhood in Lousville, Winston Co., Mississippi."
else
  "No information about this year."
end

The triple equals is a length of velvet rope, checking values much like the double equals. It's just: the triple equals is a longer rope and it sags a bit in the middle. It's not as strict, it's a bit more flexible. Take the Ranges above. (1895..1913) isn't at all equal to 1905.

No, the Range (1895..1913) is only truly equal to any other Range (1895..1913). In the case of a Range, the triple equals cuts you a break and lets the Integer 1905 in, because even though it's not equal to the Range, it's included in the set of Integers represented by the Range. Which is good enough in some cases, such as the timeline I put together earlier.

My Mistake

This is what I did which caused me to write this as penance.

def local_tweet_object input
  tweet_hash = case input.class # the screw-up
  when String
    MultiJson.decode input
  when Hash
    input
  else
    fail ArgumentError.new(
      "Cannot process #{input.class} only String or Hash")
  end
  LocalTweet.new tweet_hash
end

This code was raising the following error.

ArgumentError: I don't want Hash give me only String or Hash ... huh?

At first glance it looked fine, but then we remember that case-when uses ===. Again, we think "this shouldn't be an issue, if it's good enough for == it should be fine for ===." But === behaves specially for certain types in Ruby such as Classes, Arrays, and as we saw before, Ranges.

For simple scalar values === acts like you expect.

12 === 12 #=> true
12 === 13 #=> false
:rats === :rats #=> true
"pens" === "pens" #=> true
"space" === "fact" #=> false

And we know how === treats ranges

(8..64) === 32 #=> true

But note that

(8..64) === (8..64) #=> false

Suddenly!

Integer === 12 #=> true!

waitaminute.. what?

So Ruby can tell that 12 is a type of Integer and thus === can be described partially as an includes and typeof operator. But there are some further gotchas.

[ 1, 2, 3 ] === 1#=> true
[ 1, 2, 3 ] === [ 1, 2, 3 ]                         #=> true
[ 1, 2, 3 ] === [ 1, 2 ]                            #=> false
{ :foo => :bar } === { :foo => :bar }               #=> true
{ :foo => :bar } === :foo                           #=> false
{ :foo => :bar, :baz => :qux } === { :foo => :bar } #=> false

==='s behavior isn't completely intuitive even within Ruby's standard Classes. Which brings us (finally) back around to my original error. Hash === Hash #=> false

The fix is simple, just change

case input.class

to

case input

I posted on identi.ca a friendly notice about this easy mistake and was asked to provide clarification. So here, just over 24 hours late, it is. The full source code is provided here