Ruby now has three distinct mechanisms for matching values
case/when: the older form, based on===.case/in: the newer structural pattern matching (Ruby 2.7+).=>operator: an inline pattern assertion that raises if it fails.
That’s the high-level summary. Each behaves differently and is worth knowing in detail.
After programming in Ruby for years, I only recently realized its depth. There is much more to it than I had assumed. This write-up is my attempt to sort it out. And if I’ve gotten something wrong, I’d appreciate being corrected.
1. case/when
The original case statement:
case value
when pattern
...
when other
...
end
when uses the === operator. A few examples:
Integer === 5 # → true
/foo/ === "foobar" # → true
"abc" === "abc" # → true
Because === can be redefined, this case/when is flexible but not structural. It simply chains === checks.
- Flexible: because you can override
===in your own classes. That letscase/whenwork with regexes, ranges, and custom matchers. - Not structural: because it doesn’t deconstruct arrays or hashes. It doesn’t look “inside” values. It only asks “does
pattern === valuereturntrue?”.
2. case/in
Ruby 2.7 introduced in, which has its own set of matching rules:
case value
in Integer
puts "integer"
in [a, b]
puts "array with #{a} and #{b}"
in {amount:, currency:}
puts "price: #{amount} #{currency}"
end
Key differences:
- Classes like
Integeract as type checks. - Arrays and hashes destructure.
- Variables bind directly (
a,b,amount). - Guards are supported (
in Integer if value > 0).
This is intended for structural matching and destructuring, not for leveraging ===.
3. =>
Ruby also added an inline match operator:
value => pattern
This applies the same rules as in, but without fallbacks:
- On success, variables bind.
- On failure, Ruby raises
NoMatchingPatternError.
Examples:
[1, 2] => [a, b]
# a = 1, b = 2
123 => Integer
# success
"abc" => Integer
# raises NoMatchingPatternError
This is most useful for quick type checks and destructuring in argument handling or validation code.
A common idiom is to use => as a lightweight type guard inside constructors or method definitions. This replaces the more verbose is_a? checks with a single line that asserts the type and raises if it doesn’t match:
class Price
def initialize(amount:, currency:)
amount => Integer
currency => String # or a custom Currency class
@amount = amount
@currency = currency
end
end
Price.new(amount: 5, currency: "USD") # works
Price.new(amount: "five", currency: "USD") # raises NoMatchingPatternError
This style is concise, enforces runtime checks in a visible way, and avoids scattering raise unless amount.is_a?(Integer) throughout your code. It’s not a substitute for static typing, but it provides a small guardrail in places where correctness matters.
4. Practical Use
- Use
case/whenwhen you want===semantics (regexes, ranges, classes). - Use
case/inwhen parsing JSON, keyword args, or other structured data. - Use
=>for assertions and destructuring where a wholecaseblock would be overkill.
Summary
case/when: old, based on===.case/in: new, structural pattern matching.=>operator: inline match, raises on failure.
Leave a comment