A pin compares; a bare name binds¶
Level: 201 · anyone whose in expected matched everything
One line: Inside a pattern a bare name always binds — in expected matches anything and overwrites expected — so comparing with a variable takes the pin operator, ^expected, and ^(expr), ^@ivar and ^$gvar pin other things; guards add if/unless, | joins alternatives, and the parser refuses a repeated name and a named variable inside an alternative — Python has the same capture rule, a dotted name instead of a pin, and looser rules for |.
A pattern is built from a few kinds of node, and the rule that trips everyone is the first: an identifier is a binding, never a comparison. case 7 in expected does not ask whether 7 == expected; it matches anything and assigns 7 to expected. To compare with a variable you pin it: in ^expected. The pin also takes a parenthesised expression, an instance variable or a global — ^(limit * 2), ^@limit, ^$max — while a constant needs no pin because a constant is a value pattern, tested with ===, like in Integer. Because binding and pinning can share one pattern, in [a, ^a] binds the first element and demands that the second equal it. A guard is an if or unless after the pattern; | joins alternatives and => name binds whichever matched; and two things do not compile: the same name twice in one pattern, and a named variable inside an alternative (an underscore-prefixed one is allowed). The program measures each rule, with the two syntax errors measured by compiling a string.
Verified output of pin_guards_and_alternatives_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. 5 in ^expected (expected = 5) -> true; 6 in ^expected -> false
2. case 7; in expected -> matches anything; expected is now 7
3. [3, 3] / [3, 4] in [a, ^a] -> true / false; in [x, x] -> SyntaxError
4. [[1, 2], 2] / [[1, 2], 3] in [[_, x], ^x] -> true / false
5. ^(2 * 3), ^@limit, ^$max -> 6 in ^(2 * 3) -> true; Gate(10): at the limit / under / over; 9 in ^$max -> true
6. 5 / 6 in LIMIT (a constant) -> true / false; in ^1 -> SyntaxError; in ^Integer -> SyntaxError
7. in [x, y] if x > y / unless x > y -> descending / not descending
8. 2.5 in Integer | Float => num -> "number 2.5"
9. [x, y] | [x, y, _] / [_x, _y] | [_x, _y, _] -> SyntaxError / matched, _x = 1
10. def limit = 5; case 8; in limit -> binds a local: limit = 8; in ^limit -> SyntaxError; 5 in ^(limit()) -> true
Reading the output¶
Row 1 is the pin doing its job: 5 in ^expected is true and 6 in ^expected is false, with expected at 5. Row 2 is the trap: case 7 in expected matched, and expected is now 7 — the bare name bound. Row 3 combines the two in one pattern: [a, ^a] binds a to the first element and pins it for the second, so [3, 3] matches and [3, 4] does not; writing [x, x] instead is a SyntaxError, because a name may be bound once per pattern. Row 4 shows the pin reaching into a nested pattern: [[_, x], ^x].
Row 5 is the other three pins. ^(2 * 3) pins any expression; inside Gate#check, ^@limit pins an instance variable, so the three calls sort 10, 3 and 12 into at-the-limit, under and over; ^$max pins a global. Row 6: a constant such as LIMIT is a value pattern and needs no pin — 5 in LIMIT is true — and the pin refuses a literal (^1) and a constant (^Integer), because there is nothing they could be confused with. Row 7: guards. in [x, y] if x > y and in [x, y] unless x > y split pairs into descending and not. Row 8: Integer | Float => num matches either class and binds the value once. Row 9: [x, y] | [x, y, _] is a SyntaxError — a named variable is not allowed inside an alternative, since it might be left unbound — while [_x, _y] | [_x, _y, _] compiles, and _x was bound. Row 10 is row 2 again, with a method in the way: with def limit defined, in limit still binds a new local named limit, ^limit is a syntax error because there is no local to pin, and ^(limit()) pins the method's value.
Compared with Python¶
Python's capture rule is the same: a bare name in a case line matches anything and binds it (row 2), and because such a case can never fail, any case after it is a SyntaxError, because the later patterns could never be reached. There is no pin (row 1): comparing with a variable is a guard, case x if x == expected, and a repeated name is refused both flat and nested (rows 3 and 4), with a guard again doing the equality test. What Python has instead of the pin is the value pattern: any dotted name — self.limit, K.LIMIT, Color.RED — is compared, not bound (rows 5 and 6), which is why a module-level LIMIT captures while K.LIMIT compares. Guards take only if (row 7); unless is a syntax error, and if not does the job. Alternatives use | with as for the binding (row 8), and are looser than Ruby's: [x, y] | [x, y, _] compiles, because every alternative binds the same names, and only [x, y] | [x, z] is refused (row 9). Row 10: a function name captures like any other bare name — after case limit: the name limit is the number 8 — and case limit(): is a class pattern, which is a different thing entirely.
Verified output of pin_guards_and_alternatives_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. 5 in ^expected (expected = 5) -> SyntaxError for case ^expected:; guard `case x if x == expected` -> matched / no
2. case 7; in expected -> matches anything; expected is now 7; a case after it is a SyntaxError
3. [3, 3] / [3, 4] in [a, ^a] -> SyntaxError for case [a, a]:; guard `[a, b] if a == b` -> True / False
4. [[1, 2], 2] / [[1, 2], 3] in [[_, x], ^x] -> SyntaxError for [[_, x], x]; guard -> True / False
5. ^(2 * 3), ^@limit, ^$max -> 6 via guard `if x == 2 * 3` -> True; Gate(10) via `case self.limit:`: at the limit / under / over; 9 via guard `if x == MAX` -> True
6. 5 / 6 in LIMIT (a constant) -> case LIMIT: captures (a case after it is a SyntaxError); case K.LIMIT: -> True / False
7. in [x, y] if x > y / unless x > y -> descending / not descending; `unless` guard -> SyntaxError
8. 2.5 in Integer | Float => num -> 'number 2.5'
9. [x, y] | [x, y, _] / [_x, _y] | [_x, _y, _] -> compiles (same names); [x, y] | [x, z] -> SyntaxError; matched, x = 1
10. def limit = 5; case 8; in limit -> rebinds the name: limit = 8; `case limit():` -> compiles
The Python library has no page on match; the rules for capture, value and | patterns are in PEP 634 ↗, and PEP 636 ↗ walks through them.
Try it¶
- Write
same_pair?(pair)within [a, ^a], thenband(n, limit)that answers "exactly the limit" via^limit, "exactly double" via^(limit * 2), "below" via a guard and "above" otherwise. Finish with the trap: setlimit = 10, runcase 9 in limit, and print whatlimitis afterwards. - Write a unit converter with
case [amount, unit] in [Numeric => n, "km" | "m" => u]— the alternative bindsuonce — then try to write it asin ["km" => u] | ["m" => u]and read the error the parser gives you.
Solution to 1
pin_guards_and_alternatives_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: a pair-of-equals test with [a, ^a], a band check that pins a
# variable and an expression, and the shadowing trap of a bare name.
def same_pair?(pair) = (pair in [a, ^a])
def band(n, limit)
case n
in ^limit then "exactly the limit"
in ^(limit * 2) then "exactly double"
in Integer if n < limit then "below"
else "above"
end
end
[[3, 3], [3, 4], ["x", "x"]].each do |pair|
puts format("same_pair?(%-10s) -> %s", pair.inspect, same_pair?(pair))
end
[10, 20, 4, 15].each do |n|
puts format("band(%2d, 10) -> %s", n, band(n, 10))
end
limit = 10
case 9
in limit then nil
end
puts "after `case 9; in limit`, limit is #{limit} -- the bare name rebound it"
Verified output of pin_guards_and_alternatives_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
case/inmatches shape, not just value — the pattern forms these pins, guards and alternatives sit insidedeconstructanddeconstruct_keyslet any object be matched — a pin works the same insidePoint[x, ^y]- Nothing matched:
case/inraises,case/whenreturnsnil— what a guard that never passes leads to case/whenasks===, and every class answers it its own way — where a constant's value-pattern test,===, comes from- Closures capture variables — the same rule about a bare name and an existing local, seen from a block
- Rust: match guards ↗ —
ifafter a pattern in a language where the compiler checks exhaustiveness - Rust: binding with
@↗ — Rust'sname @ patternis Ruby'spattern => name - Ruby docs: pattern matching ↗ — the pin, guards and alternatives in the reference