Skip to content

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

  1. Write same_pair?(pair) with in [a, ^a], then band(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: set limit = 10, run case 9 in limit, and print what limit is afterwards.
  2. Write a unit converter with case [amount, unit] in [Numeric => n, "km" | "m" => u] — the alternative binds u once — then try to write it as in ["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.

same_pair?([3, 3]    ) -> true
same_pair?([3, 4]    ) -> false
same_pair?(["x", "x"]) -> true
band(10, 10) -> exactly the limit
band(20, 10) -> exactly double
band( 4, 10) -> below
band(15, 10) -> above
after `case 9; in limit`, limit is 9 -- the bare name rebound it

See also