case/in matches shape, not just value¶
Level: 201 · anyone who has written if h[:type] == "click" && h.key?(:x) three times in a row
One line: case … in tests a value against a pattern — an array shape, a hash shape with the keys you name, a class, a nested mix of them — and binds variables as it goes; Python's match/case (3.10) does the same with sequence, mapping, class and capture patterns, minus Ruby's find pattern, its one-line value => pattern and its boolean value in pattern.
Where case/when asks each candidate candidate === subject, case/in asks whether the subject has the shape of a pattern. [Integer => a, *rest] matches an array whose first element is an Integer, binds it to a and the remainder to rest. {name: String => n, age:} matches a hash that has a :name holding a String and an :age, binds both, and ignores any other keys unless **nil says there must be none. Patterns nest to any depth, a find pattern [*, "admin", *post] looks for an element anywhere in an array, | joins alternatives, and the same patterns work outside case: config => {host:, port:} matches or raises, and value in pattern is a boolean. Any object can take part — a Struct or Data answers both array and hash patterns, and the next lessons show how a class of your own joins in. The program measures each form.
Verified output of case_in_pattern_matching_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. [1, 2, 3] in [Integer => a, *rest] -> a = 1, rest = [2, 3]
2. person in {name: String => n, age:} -> n = "Ada", age = 36 (city: ignored)
3. {name:, **nil} vs {name:, **rest} -> extra: [:age, :city] / exact
4. nested {user: {roles: [String => first, *]}} -> n = "Ada", first = "dev"
5. find pattern [*, "admin", *post] -> post = ["ops"]
6. alternatives [_, _] | [_, _, _] -> "pair or triple"
7. rightward config => {host:, port:} -> host = "localhost", port = 80
8. boolean: value in pattern -> {a: 1} in {a: Integer} -> true; 5 in String -> false; 5 in Integer => five binds five = 5
9. nil pattern -> "matched nil"
10. Struct: in [x, y] / in {x:, y:} -> [1, 2] / [1, 2]
11. Data: {lat:, lng:} / Coord[a, b] / Coord(k:) -> [1.5, 2.5] / [1.5, 2.5] / [1.5, 2.5]
12. one line: case 5 in X / case 5; in X -> SyntaxError / compiles
13. hash pattern keys are symbols -> in {"name" => n} -> SyntaxError; {"name" => "Ada"} in {name:} -> false
Reading the output¶
Row 1 is the array pattern: Integer => a is a class check and a binding, and *rest takes the remaining elements. Row 2 is the hash pattern: person has three keys, the pattern names two, and the match succeeds with city: ignored — a hash pattern is a subset test. Row 3 shows the two ways to say something about the keys you did not name: **nil demands that there are none, so the three-key hash fell through to the second branch, whose **rest collected [:age, :city], while the one-key hash was "exact". Row 4 nests a hash pattern inside a hash pattern inside an array pattern, and row 5 is the find pattern: [*, "admin", *post] scanned for "admin" and bound what followed it.
Row 6 joins two patterns with | — with _ placeholders, because a named variable inside an alternative is a syntax error (the next lesson measures it). Row 7 is the rightward form: config => {host:, port:} is a whole pattern match on one line, binding two locals; it raises when the shape is wrong (two lessons on). Row 8 is the boolean form, value in pattern, which returns true or false and, as 5 in Integer => five shows, still binds. Row 9: nil is a pattern like any literal.
Rows 10 and 11: a Struct answers an array pattern and a hash pattern, and a Data object answers a hash pattern as well as the two forms that also check the class — Coord[lat, lng] and Coord(lat:, lng:). Row 12 is a parsing trap: on one line, case 5 in Integer reads 5 in Integer as row 8's boolean test and is a SyntaxError; the subject needs a ; or a newline before in. Row 13: the keys in a hash pattern are symbols, {"name" => n} does not even compile, and a string-keyed hash never matches {name:}.
Compared with Python¶
Python's match (PEP 634) has the same four families: rows 1 and 4 are sequence patterns with int() as a for check-and-bind and *rest for the remainder, row 2 a mapping pattern that likewise ignores extra keys, rows 10 and 11 class patterns — Point(x, y) positional through __match_args__, Coord(lat=lat, lng=lng) by keyword — and row 6 alternatives with |. The differences are in what is missing. Row 3: **rest collects extra keys but there is no **nil; forbidding extras takes a guard. Row 5: a sequence pattern allows one *, so the find pattern is a SyntaxError and a guard with in does the search. Row 7: there is no one-line form — match is a block statement, as row 12 also shows — so a single-shape destructuring is a match with one case. Row 8: in is the membership test, so 5 in int is a TypeError; the boolean question is isinstance. Row 11 adds a difference in the other direction: a dataclass is not a sequence, so [lat, lng] does not match it, where Ruby's Data answers [a, b] through Coord[lat, lng]. Row 13 is the one freedom Python has and Ruby lacks: mapping-pattern keys are any literal, so {"name": n} matches a string-keyed dict.
Verified output of case_in_pattern_matching_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. [1, 2, 3] in [Integer => a, *rest] -> a = 1, rest = [2, 3]
2. person in {name: String => n, age:} -> n = 'Ada', age = 36 (city ignored)
3. {name:, **nil} vs {name:, **rest} -> extra: ['age', 'city'] / exact (guard `if not rest`; {**None} is a SyntaxError)
4. nested {user: {roles: [String => first, *]}} -> n = 'Ada', first = 'dev'
5. find pattern [*, "admin", *post] -> SyntaxError for two stars; guard `if "admin" in roles` -> post = ['ops']
6. alternatives [_, _] | [_, _, _] -> 'pair or triple'
7. rightward config => {host:, port:} -> SyntaxError; match with one case -> host = 'localhost', port = 80
8. boolean: value in pattern -> 5 in int -> TypeError (membership test); isinstance(5, str) -> False
9. nil pattern -> 'matched None'
10. Struct: in [x, y] / in {x:, y:} -> [1, 2] via [x, y] / [1, 2] via Point(x, y)
11. Data: {lat:, lng:} / Coord[a, b] / Coord(k:) -> [1.5, 2.5] via Coord(lat=, lng=) / [1.5, 2.5] via Coord(lat, lng) / [lat, lng] -> 'no match'
12. one line: case 5 in X / case 5; in X -> SyntaxError / compiles
13. hash pattern keys are symbols -> case {"name": n}: matches a str-keyed dict -> n = 'Ada'
The Python library has no page on match; PEP 636 ↗ is the tutorial and PEP 634 ↗ the specification.
Try it¶
- Dispatch a list of event hashes —
{type: "click", x:, y:},{type: "key", key:}with an optionalmodifiers:array,{type: "resize", size: [w, h]}, and one unknown — with onecase/inper shape, binding the fields each shape carries; use a find pattern to spot"ctrl"among the modifiers, and anelsefor the rest. - Pull
hostandportout of{server: {host: "h", port: 1}, tls: false}with one rightward=>, then guard anifwithconfig in {tls: false => tls}and print whattlswas bound to.
Solution to 1
case_in_pattern_matching_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: dispatch a list of event hashes with case/in, one pattern per
# shape, binding the fields each shape carries, with an else for the rest.
events = [
{type: "click", x: 10, y: 20},
{type: "key", key: "q", modifiers: ["ctrl"]},
{type: "resize", size: [80, 24]},
{type: "key", key: "x"},
{type: "boom"}
]
events.each do |event|
line = case event
in {type: "click", x:, y:} then "click at #{x},#{y}"
in {type: "key", key:, modifiers: [*, "ctrl", *]} then "ctrl-#{key}"
in {type: "key", key:} then "key #{key}"
in {type: "resize", size: [w, h]} then "resize to #{w}x#{h}"
else "unknown event #{event[:type].inspect}"
end
puts line
end
Verified output of case_in_pattern_matching_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
case/whenasks===, and every class answers it its own way — the oldercase, which tests values rather than shapes- A pin compares; a bare name binds —
^expected, guards, and why|refuses a named variable deconstructanddeconstruct_keyslet any object be matched — how a class of your own answers[x, y]and{x:, y:}- Nothing matched:
case/inraises,case/whenreturnsnil— what rows 7 and 9 do when the shape is wrong - Everything is an expression, even
defandclass—case/inis a value, which is whatshape = case …relies on - Struct and Data — the two record types of rows 10 and 11
- Destructuring assignment —
a, *rest = arr, the assignment-side cousin of an array pattern - Rust: destructuring structs ↗ — the same idea with a compiler that checks every field
- Ruby docs: pattern matching ↗ — the reference for every pattern form
Timeis an instant,Dateis a calendar day — aTimeis a point on the timeline with an offset attached- What changed, 1.9 to 4.0 — Ruby's releases each added a few things you will meet in old code and new
- There is more than one way, and one obvious way — Ruby's idioms