Pattern Matching
Beamtalk supports destructuring assignment for arrays, maps, and Erlang tuples.
Unlike full Smalltalk, these are assignment patterns — not match expressions
with multiple branches (for branch dispatch use match:, later in this chapter).
Forms:
#[a, b] := array — array destructuring
#{#key => var} := dict — map destructuring
{a, b} := tuple — Erlang tuple destructuring
match: [...] — multi-branch pattern dispatch
_ (underscore) is the wildcard — it matches and discards.
Array destructuring
TestCase subclass: Ch14ArrayDestructuring
testBasic =>
#[a, b] := #[10, 20]
self assert: a equals: 10
self assert: b equals: 20
testThreeElements =>
#[a, b, c] := #[1, 2, 3]
self assert: a equals: 1
self assert: b equals: 2
self assert: c equals: 3
testWildcard =>
// _ discards the matched element:
#[_, b] := #[99, 42]
self assert: b equals: 42
testExtraElementsIgnored =>
// Pattern has fewer slots than array — extras are silently ignored:
#[a, b] := #[10, 20, 30, 40]
self assert: a equals: 10
self assert: b equals: 20
testStrings =>
#[first, second] := #["hello", "world"]
self assert: first equals: "hello"
self assert: second equals: "world"
testSequential =>
#[a, b] := #[1, 2]
#[c, d] := #[3, 4]
self assert: a + b + c + d equals: 10
testInCollectBlock =>
// Destructuring inside iteration blocks:
pairs := #[#[1, 10], #[2, 20], #[3, 30]]
sums := pairs collect: [:pair |
#[a, b] := pair
a + b
]
self assert: sums equals: #[11, 22, 33]
testInSelectBlock =>
result := #[#[1, 10], #[15, 5], #[2, 20]]
select: [:pair |
#[a, b] := pair
a < b
]
self assert: result equals: #[#[1, 10], #[2, 20]]
testInInjectBlock =>
result := #[#[1, 10], #[2, 20]]
inject: 0
into: [:acc :pair |
#[a, b] := pair
acc + a + b
]
self assert: result equals: 33
Map destructuring
TestCase subclass: Ch14MapDestructuring
testSingleKey =>
dict := #{#name => "Alice"}
#{#name => name} := dict
self assert: name equals: "Alice"
testMultipleKeys =>
dict := #{#x => 10, #y => 20}
#{#x => x, #y => y} := dict
self assert: x equals: 10
self assert: y equals: 20
testWildcard =>
dict := #{#sid => "abc123", #runner => "runner1"}
#{#sid => sid, #runner => _} := dict
self assert: sid equals: "abc123"
testSubsetDestructuring =>
// Only some keys — extra keys in the dict are ignored:
dict := #{#a => 1, #b => 2, #c => 3}
#{#a => a, #c => c} := dict
self assert: a equals: 1
self assert: c equals: 3
testMissingKeyRaises =>
self should: [
#{#missing => _val} := #{#present => 42}
] raise: RuntimeError
testInCollectBlock =>
dicts := #[#{#x => 1, #y => 10}, #{#x => 2, #y => 20}]
sums := dicts collect: [:d |
#{#x => x, #y => y} := d
x + y
]
self assert: sums equals: #[11, 22]
Erlang tuple destructuring
Beamtalk can destructure Erlang tuples using {} pattern.
This is mainly useful when interoperating with Erlang code that returns
{ok, Value} or {error, Reason} tuples.
TestCase subclass: Ch14TupleDestructuring
testBasic =>
{a, b} := Erlang erlang list_to_tuple: #[10, 20]
self assert: a equals: 10
self assert: b equals: 20
testTaggedTuple =>
// Common Erlang pattern: {ok, Value} or {error, Reason}
{status, value} := Erlang erlang list_to_tuple: #[#ok, 42]
self assert: status equals: #ok
self assert: value equals: 42
testLiteralPatternInFirstSlot =>
// A literal in the pattern matches only that value:
{#ok, value} := Erlang erlang list_to_tuple: #[#ok, 42]
self assert: value equals: 42
testLiteralMismatchRaises =>
// Mismatch raises a RuntimeError (badmatch):
self should: [
{#ok, _val} := Erlang erlang list_to_tuple: #[#error, 42]
] raise: RuntimeError
testWildcard =>
{_, b} := Erlang erlang list_to_tuple: #[#ignore, 99]
self assert: b equals: 99
The match: expression
For multi-branch dispatch on a value, use match:. This is more powerful
than destructuring assignment — it tests multiple patterns and runs the
first matching branch.
Literal and variable patterns
TestCase subclass: Ch14MatchPatterns
testLiteralMatch =>
result := #ok match: [
#ok -> "success";
#error -> "failure";
_ -> "unknown"
]
self assert: result equals: "success"
testVariableCapture =>
result := 42 match: [x -> x + 1]
self assert: result equals: 43
Guard expressions (when:)
Add a when: guard to constrain when an arm matches. The guard is a
block that must evaluate to true for the arm to fire:
testGuardMatch =>
result := 10 match: [
x when: [x > 0] -> "positive";
_ -> "non-positive"
]
self assert: result equals: "positive"
testMultipleGuards =>
result := 42 match: [
x when: [x > 100] -> "big";
x when: [x > 10] -> "medium";
_ -> "small"
]
self assert: result equals: "medium"
Array patterns in match:
testArrayPatternInMatch =>
result := #[10, 20] match: [
#[h, t] -> h + t;
_ -> 0
]
self assert: result equals: 30
testArrayPatternWithGuard =>
result := #[1, 2] match: [
#[x, y] when: [x < y] -> "ascending";
_ -> "other"
]
self assert: result equals: "ascending"
Nested patterns
Patterns can nest — match arrays within arrays:
testNestedArrayPattern =>
result := #[#[1, 2], 3] match: [
#[#[a, b], c] -> a + b + c;
_ -> 0
]
self assert: result equals: 6
Map patterns in match:
testMapPatternInMatch =>
d := #{#event => "click", #x => 5}
result := d match: [
#{#event => evName} -> evName;
_ -> "unknown"
]
self assert: result equals: "click"
Constructor patterns (Result)
Match on Result ok: and Result error: arms:
testConstructorPattern =>
result := (Result ok: 42) match: [
Result ok: v -> v;
Result error: _ -> 0
]
self assert: result equals: 42
testConstructorPatternWithGuard =>
result := (Result ok: 5) match: [
Result ok: v when: [v > 3] -> "big";
Result ok: _ -> "small";
Result error: _ -> "err"
]
self assert: result equals: "big"
Duplicate variables as equality constraints
Repeating a variable name in a pattern constrains both positions to be equal:
testDuplicateVariableEquality =>
result := #[3, 3] match: [
#[x, x] -> "equal";
_ -> "differ"
]
self assert: result equals: "equal"
Binary pattern matching (match:)
For matching on binary data (strings as UTF-8 bytes, binary protocols),
use the match: expression with << >> binary segments.
Syntax:
expr match: [
<<segment, ...>> -> result;
<<segment, ...>> when: [guard] -> result;
_ -> fallback
]
Segment types: name:8 (8-bit int), name/binary (rest as binary)
TestCase subclass: Ch14BinaryPatterns
testMatchFirstByte =>
// "A" is byte 65 in UTF-8:
result := "ABC" match: [
<<first:8, _/binary>> -> first;
_ -> 0
]
self assert: result equals: 65
testMatchRestBinary =>
result := "ABC" match: [
<<_:8, rest/binary>> -> rest size;
_ -> 0
]
self assert: result equals: 2
testMatchWildcard =>
result := "hello" match: [
<<_/binary>> -> true;
_ -> false
]
self assert: result
testMatchWithGuard =>
result := "X" match: [
<<first:8, _/binary>> when: [first > 200] -> "high";
_ -> "low"
]
self assert: result equals: "low"
Summary
Destructuring assignment:
#[a, b, c] := someArray — array
#{#key => var} := dict — map
{a, b} := erlangTuple — tuple
match: expression:
expr match: [
literal -> result; — literal match
x -> result; — variable capture
x when: [guard] -> result; — guarded match
#[a, b] -> result; — array pattern
#[#[a, b], c] -> result; — nested pattern
#{#key => v} -> result; — map pattern
Result ok: v -> result; — constructor pattern
#[x, x] -> result; — equality constraint
<<byte:8, rest/binary>> -> r; — binary pattern
_ -> fallback — wildcard
]
Wildcard: _ (matches and discards any value)
Exercises
1. Nested destructuring. Given pairs := #[#[1, 2], #[3, 4]], use
destructuring inside inject:into: to sum all four values.
Hint
pairs := #[#[1, 2], #[3, 4]]
pairs inject: 0 into: [:acc :pair |
#[a, b] := pair
acc + a + b
]
// => 10
2. match: with guards. Write a match: expression that classifies a number
as "even" or "odd" using a guard that checks isEven.
Hint
7 match: [
x when: [x isEven] -> "even";
_ -> "odd"
]
// => "odd"
3. Map pattern matching. Given a dictionary #{#event => "click", #x => 5, #y => 10},
use a map pattern in match: to extract the event name.
Hint
d := #{#event => "click", #x => 5, #y => 10}
d match: [
#{#event => name} -> name;
_ -> "unknown"
]
// => "click"
Map patterns match a subset of keys — extra keys are ignored.
Next: Chapter 15 — Type Annotations