How to Write a Case Expression in Ruby

How to Write a Case Expression in Ruby

Imagine you need a way to check the value of an object, and perform a different action based on that. The Object-Oriented Programming way is to use polymorphism, and we will see how to do that in a second.

But first, let’s look at how the case expression works, and how you can use it to achieve the goal mentioned above.

How does case work?

There are two main parts to the case expression. The case clause, and the when clause.

The when clause is the expression that defines the receiver of the operator (defaulting to ===), and the case clause defines the argument passed to it.

Let’s see an example.

case a
when String
  #..
end

So you can think about the above example as String === a, or String.===(a).

Finally, there’s also an else clause that you can use for when there is no match.

case my_object
when String
  "This is a string"
else
  "I have no idea what this is"
end

The triple equals operator (===)

By default, case uses the triple equals (===) operator to do the comparison.

The thing with === is it has nothing to do with equality. By default, it’s aliased to the double equals operator (==) which normally checks if two objects have equivalent value, but it can be defined to mean anything.

For example, a range defines it as an alias for includes?, a regex defines it as an alias for match, a class for is_a?, a proc for call. You get the idea.

That operator works as expected with literals, but not with classes:

1 === 1              # => true
Numeric === Numeric # => false

That is because the === is an alias for kind_of?. And here are the docs for kind_of?.

kind_of?(class) → true or false

Returns true if class is the class of obj, or if class is one of the superclasses of obj or modules included in obj.

This means that if you want to do a case ... when over an object’s class, this will not work:

obj = 'Hi'
case obj.class
when String
  "It's a string"
end

That is because, it translates to String === String. And that returns false.

The default operator

The case expression has a default operator. And that means you don’t have to specify it. That’s very nice, because you can just write when String and it defaults to the === operator.

But, there are times when you want to use a different operator. And Ruby allows you to do it. Here’s how.

case
when a < 3
  "Smaller than 3"
end

Case is an expression

Just like the name says, the whole thing is an expression. That means, it evaluates to a value. So you can assign the result of the entire case expression to a variable, like so.

value = case
        when a < 3
          "Smaller than 3"
        end

There is no fall-through

If you’re coming from other languages, you’ve probably used a switch statement or something similar, and you’ve noticed that case doesn’t use keywords like break to break the flow.

That is because there is no fall-through with case. It just returns the value of the expression that was matched and that’s it.

Multiple matches

Up until now, you’ve only used one value for the when clause. But you can use multiple values.

case a
when 1..3
  "Small number"
end

Matching regexes

You can use case expressions to match anything. But just in case it’s not obvious, you could also match regexes, or lambdas. Here’s an example.

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

case a
when even
  "It's even"
when /^[0-9]+$/
  "It's an integer"
end

Define your own

On of the lesser known facts is that you can define your own comparators.

Text = Struct.new(:min_length) do
  def ===(string)
     string.size > min_length && string.is_a?(String)
  end
end

case a
when Text.new(100)
  "It's text"
end

In the example above, if a is a string of more than 100 characters, then the case expression will return It's text.

Use polymorphism instead

I’m not going to get into the benefits of OOP here, you can read more about that in the Object-Oriented Programming with Ruby article, but I am going to show you how you can use polymorphism to replace a case expression.

The case version

class Person
  attr_reader :country

  def initialize(country)
    @country = country
  end

  def nationality
    case country
    when "USA"
      "This guy is an American"
    when "Romania"
      "This guy is a Romanian"
    end
  end
end

john = Person.new("USA")
puts john.nationality # => This guy is an American

The OOP version

class Person
  attr_reader :country

  def initialize(country)
    @country = country
  end

  def nationality
    country.nationality
  end
end

class America
  def nationality
    "This guy is an American"
  end
end

class Romania
  def nationality
    "This guy is a Romanian"
  end
end

john = Person.new(America.new)
puts john.nationality # => This guy is an American

It’s a good idea to delete as much flow control code (i.e. ifs, and case expressions) as possible. It improves adherence to the Single Responsibility Principle and makes the program more readable in general.

If you liked this article, please take a moment and say thanks by sharing it on your favorite social media channel.