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. if
s, and case
expressions) as possible. It improves adherence to the Single Responsibility Principle and makes the program more readable in general.