Created
July 28, 2011 19:28
-
-
Save nuclearsandwich/1112335 to your computer and use it in GitHub Desktop.
A Look at Case Expressions in Ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A Look at Case Expressions in Ruby | |
# ================================== | |
# | |
# *Side note: Playing with docco as a formatting tool for this article. Excuse | |
# the lines with solitary* `:parabreak` *symbols, they're to make paragraphs on the | |
# right hand side.* | |
# | |
# 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. | |
:parabreak | |
# 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`. | |
:parabreak | |
# ## 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. | |
# ### From the Guide: | |
:parabreak | |
# > 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 | |
:parabreak | |
# This is what I did yesterday morning 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( | |
"I don't want #{input.class} give me 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 | |
# Oh. Well now that I know, I need only change the case from | |
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](https://gist.github.com/1112335) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note also that this is not commutative.