Ruby: The Case for Case

It’s nice to have case and when available in Ruby. But I don’t see it used too often. I myself don’t use it much because I’m more accustomed to just using if and else. So let’s get into case switching.

person = "employee" case person when "boss" puts "Yes sir!" when "employee" puts "Sure I'll get around to that if I have any time." else puts "*smile and nod*" end # => "Sure I'll get around to that if I have any time." 1 2 3 4 5 6 7 8 9 10 11 12 person = "employee" case person when "boss" puts "Yes sir!" when "employee" puts "Sure I'll get around to that if I have any time." else puts "*smile and nod*" end # => "Sure I'll get around to that if I have any time."

This is your standard use of case switching. We check whatever variable is handed to case against each when condition. If they match then that section of code is executed and the rest won’t be evaluated.

Alternatively you can just not hand a value to case and use when like you would with if and else.

dollars = 10_000_000 case when dollars > 999_999 puts "You're a millionaire!" when dollars == 0 puts "You're flat broke!" else puts "You are not flat broke, nor are you a millionaire." end # => "You're a millionaire!" 1 2 3 4 5 6 7 8 9 10 11 12 dollars = 10_000_000 case when dollars > 999_999 puts "You're a millionaire!" when dollars == 0 puts "You're flat broke!" else puts "You are not flat broke, nor are you a millionaire." end # => "You're a millionaire!"

Those are the simplest usages for case. But wait! It gets better. You can evaluate the Object handed to case with a Proc in your when clause.

Proc it!

hour = 14 case hour when ->(a) { a.between?(14,15) } puts "It's tea time." else puts "Go on with your life." end # => "It's tea time." 1 2 3 4 5 6 7 8 9 10 hour = 14 case hour when -> ( a ) { a . between ? ( 14 , 15 ) } puts "It's tea time." else puts "Go on with your life." end # => "It's tea time."

So what happens if we do a method call as the case parameter? Lets use Time.now . I’ll use Procs with print statements to show whether we’re getting changing information.

case Time.now when ->(time){ puts time; sleep(1); false} puts "nothing" when ->(time){ puts time; sleep(1); false} puts "nothing" when ->(time){ puts time; sleep(1); false} puts "nothing" else puts "The else result was called!" end # 2015-03-03 12:22:10 -0500 # 2015-03-03 12:22:10 -0500 # 2015-03-03 12:22:10 -0500 # The else result was called! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 case Time . now when -> ( time ) { puts time ; sleep ( 1 ) ; false } puts "nothing" when -> ( time ) { puts time ; sleep ( 1 ) ; false } puts "nothing" when -> ( time ) { puts time ; sleep ( 1 ) ; false } puts "nothing" else puts "The else result was called!" end # 2015-03-03 12:22:10 -0500 # 2015-03-03 12:22:10 -0500 # 2015-03-03 12:22:10 -0500 # The else result was called!

So from this we see that the method handed to case is executed and evaluated to a hidden internal variable from which to compare. So it acts just like a variable for something handed to case. Why do I say hidden? Because there is no way to evaluate the case item inside the when block; if it’s not defined outside of case. See here:

case Time.now when ->(x){true} ->(y){puts y}.call end # ArgumentError: wrong number of arguments (0 for 1) # from (irb):79:in `block in irb_binding' # from (irb):79:in `call' 1 2 3 4 5 6 7 8 case Time . now when -> ( x ) { true } -> ( y ) { puts y } . call end # ArgumentError: wrong number of arguments (0 for 1) # from (irb):79:in `block in irb_binding' # from (irb):79:in `call'

Here we see our inner proc with y isn’t being given any arguments which is raising an error for us. So the when clause will get handed the object to evaluate, but the code within the when block doesn’t get to see it. Of course this isn’t an issue if you simply assign a variable before the case statement; then it’s available throughout.

Regex

You may use regex to evaluate your case variable.

case "asdf" when /^[A-Z]*$/ puts "upper case" when /^[a-z]*$/ puts "lower case" else puts "aaah!" end # => "lower case" 1 2 3 4 5 6 7 8 9 10 case "asdf" when / ^ [ A - Z ] * $ / puts "upper case" when / ^ [ a - z ] * $ / puts "lower case" else puts "aaah!" end # => "lower case"

Perhaps evaluating whether a string is upper case or lower case would be the most contextually appropriate use for case ;-). Just kidding of course. There are methods for that.

Caution, Weirdness, Don’t Do This

Now it is possible to change the object during the when evaluation. This can lead to none of the cases ever evaluating as true.

case "a" when ->(n){puts "'#{n.next}' == 'c'"; n.next!.equal?("c")} puts "nothing" when ->(n){puts "'#{n.next}' == 'b'"; n.next!.equal?("b")} puts "nothing" when ->(n){puts "'#{n.next}' == 'a'"; n.next!.equal?("a")} puts "nothing" else puts "The state failed to match up." end # 'b' == 'c' # 'c' == 'b' # 'd' == 'a' # The state failed to match up. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 case "a" when -> ( n ) { puts "'#{n.next}' == 'c'" ; n . next ! . equal ? ( "c" ) } puts "nothing" when -> ( n ) { puts "'#{n.next}' == 'b'" ; n . next ! . equal ? ( "b" ) } puts "nothing" when -> ( n ) { puts "'#{n.next}' == 'a'" ; n . next ! . equal ? ( "a" ) } puts "nothing" else puts "The state failed to match up." end # 'b' == 'c' # 'c' == 'b' # 'd' == 'a' # The state failed to match up.

For this same reason it’s probably not a great idea to use a Proc as a case switch with something that changes like Time.now .

case ->{Time.now} when ->(time){ puts time.call; sleep(1); false} puts "nothing" when ->(time){ puts time.call; sleep(1); false} puts "nothing" when ->(time){ puts time.call; sleep(1); false} puts "nothing" else puts "The else result was called!" end # 2015-03-03 12:24:29 -0500 # 2015-03-03 12:24:30 -0500 # 2015-03-03 12:24:31 -0500 # The else result was called! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 case -> { Time . now } when -> ( time ) { puts time . call ; sleep ( 1 ) ; false } puts "nothing" when -> ( time ) { puts time . call ; sleep ( 1 ) ; false } puts "nothing" when -> ( time ) { puts time . call ; sleep ( 1 ) ; false } puts "nothing" else puts "The else result was called!" end # 2015-03-03 12:24:29 -0500 # 2015-03-03 12:24:30 -0500 # 2015-03-03 12:24:31 -0500 # The else result was called!

So you can see that the Proc calls a fresh result from Time.now in each when case and the time has changed between each. So in this case you may never hit the when clause even though one of the when clauses may be true at one point during the evaluation. Is this likely? Probably not. But it is possible.

You can case case statements. A case statement returns a result just like anything else in Ruby and you can check another case against it. Is this useful? I don’t know. There may be an excellent use case for it somewhere in the future. But the more I think about it the more I think it’s not advisable.

case case case when true == true true end when ->(x) { return x } puts "true has been returned from the inner case" end when ->(x) { x.nil? } puts "the print statement returned nil" end # true has been returned from the inner case # the print statement returned nil 1 2 3 4 5 6 7 8 9 10 11 12 13 case case case when true == true true end when -> ( x ) { return x } puts "true has been returned from the inner case" end when -> ( x ) { x . nil ? } puts "the print statement returned nil" end # true has been returned from the inner case # the print statement returned nil

Additional Notes

I’ve touched on self recursive (tail recursive) methods once or twice. Aja Hammerly uses case statements for some of her conference talk demonstrations. It was from her conference talk up on Youtube from which I learned that you can call case without passing it something to evaluate.

In my mind it’s mostly preference that leads to using if and else over case. But at times it may be more elegant to use case. What do you think? What’s the best use case for case? Also do you know of any other behaviors or oddities with case? I’d really like to hear about it!

Hope this was both enlightening and enjoyable for you! Please feel free to comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!

God Bless!

-Daniel P. Clark

Image by Tom Godber via the Creative Commons Attribution-ShareAlike 2.0 Generic License.