Pattern matching

Ruby · Practical · 12 min

Pattern matching makes handling JSON, API responses and nested data both concise and safe.

case/in (stable since Ruby 3.0) matches a value against patterns that check structure and bind variables at the same time:

case response
in { status: 200, body: { user: { name: String => name } } }
  greet(name)
in { status: 404 | 410 }
  not_found
in { status: Integer => code } if code >= 500
  retry_later(code)
end

Building blocks:

  • Value patterns compare with ===, so classes, ranges and regexes work: Integer, 1..5, /@/.
  • Array patterns [x, y] require exact length; [first, *rest] captures the tail; the find pattern [*, { id: 3 } => hit, *] searches anywhere.
  • Hash patterns { name: } match a subset of keys and bind name. Use **rest to capture the others or **nil to forbid extras. They match symbol keys only.
  • | for alternatives, => var to bind a sub-pattern, if/unless guards.
  • ^var pins an existing variable's value instead of rebinding it: in { id: ^expected_id }.

If no branch matches and there's no else, Ruby raises NoMatchingPatternError, which surfaces unexpected data early.

One-liners: config => { db: { host: } } destructures or raises, and value in { type: :admin } returns true or false. Classes opt in by defining deconstruct and deconstruct_keys; Data and Struct already do.

Example

def describe(event)
  case event
  in { type: "click", pos: [x, y] } if x.negative? || y.negative?
    "click outside"
  in { type: "click", pos: [x, y] }
    "click at #{x},#{y}"
  in { type: "key", key: "a".."z" => k }
    "letter #{k}"
  in { type: String => t, **rest }
    "other #{t} with #{rest.keys}"
  end
end

puts describe({ type: "click", pos: [3, 4] })   # click at 3,4
puts describe({ type: "click", pos: [-1, 0] })  # click outside
puts describe({ type: "key", key: "q" })        # letter q
puts describe({ type: "scroll", dy: 5 })        # other scroll with [:dy]

config = { db: { host: "localhost", port: 5432 } }
config => { db: { host:, port: } }
puts "#{host}:#{port}"                          # localhost:5432

expected = 2
found = ([{ id: 1 }, { id: 2 }] in [*, { id: ^expected }, *])
p found                                         # true

Remember

  • case/in checks structure and binds variables in one step
  • Hash patterns match a subset of symbol keys; array patterns match exact length
  • Use ^ to pin an existing value; unmatched cases raise NoMatchingPatternError

Watch out for

  • Expecting in { x: } to match string-keyed hashes from JSON.parse; parse with symbolize_names: true.

Check yourself

  1. Does { name: "Ann", age: 30 } match the pattern in { name: String }?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ruby-lang.org/en/3.4/syntax/pattern_matching_rdoc.html

Builds on

Unlocks

Nothing builds on this yet.