Ruby's Switch Statement is More Flexible Than You Thought

Ruby's Switch Statement is More Flexible Than You Thought

Ruby's switch statement is very versatile and flexible, especially due to the dynamic nature of Ruby. In this post, we'll see how you can use it in various ways. We'll also learn why it works the way it works. Hint: it uses the `===` operator (method) under the hood.

4 min read

Here's the standard version of the switch statement in Ruby.

season = 'winter'

case season
when 'summer'
  puts 'it is warm'
when 'winter'
  puts 'it is cold'
else
  puts 'it is raining'
end

# output
# it is cold

In typical Ruby style of developer happiness, it's called case and when, instead of switch and case. I find Ruby's version easier to read and understand.

Note: There're no break statements at the end of each when clause. Unlike other languages, Ruby's case doesn't fall through.

In the standard form, we're just comparing the expression provided to case with the when expression and executing the code provided. When no matches are found, the else statement is executed.

In fact, what really happens is that Ruby compares the expression value provided in the when clause with the value in the case clause using the === operator. This lets you do fancy (but useful) stuff like this:

puts 'enter a number'
number = gets.to_i

case number
when 1..10
  puts 'between 1 and 10'
when 11..20
  puts 'between 11 and 20'
when 50
  puts 'exactly 50'
when 60, 70
  puts 'either 60 or 70'
else
  puts 'invalid number'
end

Note: The order matters. We're comparing the expression provided to the when clause with the case expression, not the other way around (when === case, not case === when). This is because Ruby actually calls the === method on the left operand, passing the second operand as an argument.

The following example shows the difference.

(1..10) === 5 # true 

5 === (1..10) # false

You can also shorten the code by returning on the same line as when clause, using the then clause.

def can_drive(age)
  case age
  when 1..14 then 'no'
  when 15..100 then 'yes'
  end
end

puts can_drive(18) # yes

Additionally, if you want to do comparisons on a variable, just leave out the case expression. This helps to simplify a long and complex if-else chain.

def calculate(num)
  case
  when num > 10
    puts 'greater than 10'
  when num == 5
    puts 'exact 5'
  end
end

calculate 12 # greater than 10

Pretty useful. Now, let's learn how you can match data types of objects.

Matching Types

The === operator lets you match the types of objects. For example,

Integer === 1     # true
String === 'name' # true

That means you can pass an object in the case clause and match types in the when clause, as follows:

def perform(obj)
  case obj
  when String
    puts 'a string'
  when Integer
    puts 'an integer'
  when Vehicle
    puts 'a vehicle'
  else
    puts 'not a string, integer, or vehicle'
  end
end

perform 5             # an integer
perform 'name'        # a string

class Vehicle
end

perform Vehicle.new   # a vehicle

Note: Do not use obj.class in the case clause, as Integer === Integer returns false. This might seem strange at first, but remember that Ruby is actually calling the === method on Integer. This method checks if the second argument is an instance of this module. Hence it returns false.

It's also possible to evaluate custom, complicated expressions via a lambda or even classes. Let's see how.

Custom Expressions

If it walks like a duck and quacks like a duck, it is a duck.

So far, we have learned that Ruby will match the when clause with the case clause using the === operator. In fact, it's calling the === method on the value returned by the when clause. Because of the dynamic nature of Ruby, you can pass any object that has a === method.

For example, a lambda has a === method which simply passes the second operand as an argument to the lambda.

even = ->(x) { x % 2 == 0 }

even === 4  # true
even === 5  # false

This means that you can pass a lambda expression in the when clause.

is_even = ->(x) { x % 2 == 0 }
is_odd = ->(x) { x % 2 == 1 }
num = 4

case num
when is_even then 'even'
when is_odd then 'odd'
end

In fact, any object that has a === method can be used in a when statement.

num = 5

class Even
  def ===(obj)
    (obj % 2) == 0
  end
end

class Odd
  def ===(obj)
    (obj % 2) == 1
  end
end

case num
when Even.new then puts 'even'
when Odd.new then puts 'odd'
end

This example is not that useful as it's oversimplified. I can't think of a real-life scenario where I'd use custom classes, but it's pretty useful to know that Ruby let's you do this.

Finally, let's see how you can match regular expressions inside case statement.

Match Regular Expressions

You can use the === operator to match a string against a regular expression.

pattern = /hello.+/ 

pattern === 'hello world' # true
pattern === 'world'       # false

As a result, you can use regular expressions in the when clause in a case statement.

case word
when /\d/
  puts 'digit'
when /[aeiou]/
  puts 'a vowel'
end

Couldn't get simpler than this.

Pattern Matching

Pattern Matching is another powerful use case for the case statement in Ruby (Credits: Thanks to the commenter on Hackernews for pointing this out). I recommend reading the official documentation, but here's a gist of it:

Pattern matching allows deep matching of structured values: checking the structure and binding the matched parts to local variables. It uses case .. in instead of case .. when.

case <expression>
in <pattern1>
  ...
in <pattern2>
  ...
in <pattern3>
  ...
else
  ...
end

For example,

config = {db: {user: 'admin', password: 'abc123'}}

case config
in db: {user:} # matches subhash and puts matched value in variable user
  puts "Connect with user '#{user}'"
in connection: {username: }
  puts "Connect with user '#{username}'"
else
  puts "Unrecognized structure of config"
end
# Prints: "Connect with user 'admin'"

It's fascinating and deserves a blog post on its own. Stay tuned!

That's a wrap. Did I miss anything? Please let me know in the comments below.


I hope you liked this article and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.