Robert Shecter

Pattern Matching in Ruby: A Wayfinder

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 lets case/when work 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 === value return true?”.

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 Integer act 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/when when you want === semantics (regexes, ranges, classes).
  • Use case/in when parsing JSON, keyword args, or other structured data.
  • Use => for assertions and destructuring where a whole case block would be overkill.

Summary

  • case/when: old, based on ===.
  • case/in: new, structural pattern matching.
  • => operator: inline match, raises on failure.

Leave a comment