pattern matching - pannous/warp GitHub Wiki
Pattern matching
Pattern matching goes beyond matching by type name: a case key describes the shape of the data, and the names in it take the parts. For keys made of values, ranges and types see switch.
Lists
A list pattern fits a list of that length; a name takes the item at its place, a value must be equal:
match ["", "x", ""] { ["", middle, ""] => middle + "!"; _ => "never mind" } // x!
Patterns nest, _ takes any item without naming it:
match [1, [2, [3]]] { [outer, [middle, [inner]]] => outer + middle + inner; _ => 0 } // 6
A rest ...rest (or *rest) takes the items the other names leave:
total(xs) := match xs { [] => 0; [first, ...rest] => first + total(rest) }
total([1, 2, 3]) // 6
Items after the rest count from the end, a bare .. takes the rest unnamed:
match [1, 2, 3] { [.., last] => last } // 3
An item pattern followed by ... fits every item of the list; each of its names takes the list of its values:
points = [1, "a"], [2, "b"](/pannous/warp/wiki/1,-"a"],-[2,-"b")
match points { [[n, label]...] => label } // ["a" "b"]
match [[1, 2], 3] { [[a, b]...] => "pairs"; _ => "no" } // no
Tuples
A tuple pattern fits a tuple of its length:
match (1, 2) { (1, t) => t; _ => 0 } // 2
match (3, 4) { (a, b) => a + b } // 7
Maps
A map pattern names only the fields it needs; the others are ignored, at any depth:
response = {status: 200, data: {user: {name: "Ann"}}}
match response { {status: 200, data: {user: {name}}} => name; _ => "?" } // Ann
{name} binds the field to its own name, {name: n} to n, {name: "Ann"} requires that value.
A unit word stays the unit: {host: h} compares host with the hour and warns; bind with {host: $h} or another name.
Instances and enum cases
Of an instance, .. leaves the other fields out:
class Point{x:int; y:int}
p = Point(3, 4)
match p { Point{x, ..} => x } // 3
An enum case with values gives them to its names:
enum Shape { Circle(r), Dot }
area(s) := match s { Circle(r) => r * r; Dot => 0 }
area(Circle(2)) // 4
Every case
A match on an enum or a sealed set of classes must name every case, or end with a catch-all:
enum Shape { Circle(r), Dot }
area(s: Shape) := match s { Circle(r) => r * r }
This is the compile error "match on Shape misses Dot", and its fix adds the missing arm or other: …. A subject of
unknown type keeps the run-time error "no case for …".
is
is tests one pattern outside a match. With names, it binds them for the then branch:
xs = [1, 2]
xs is [_, 2] // yes
if xs is [a, b] then b else 0 // 2
Map patterns work the same: if x is {age} then age else 0.
The whole value
Inside an arm the subject's name still means the whole value, and the word value means it for any subject:
xs = [1, 2]
match xs { [a, b] => xs.length } // 2
match [4, 5, 6] { [a, ...rest] => value.length } // 3
A bare name as an arm takes the value when its body uses it, and match name = … names a computed subject:
match "bob" { "alice" => "hi Al"; name => "hi " + name } // hi bob
match all = [4, 5] { [a, b] => all.length } // 2
In nested matches value is the innermost one's; a program variable named value wins.
Named patterns
The keyword pattern names a pattern, for is, where and arms; a name in it binds in the arm:
pattern Pair = [_, _]
pattern Tagged = ["tag", label, ..]
[1, 2] is Pair // yes
match ["tag", "red", 1] { Tagged => label; _ => "none" } // red
Without the keyword, Pair = [_, _] is a plain list, and x is Pair then asks for pattern Pair = ….
Conditions inside
Inside a pattern, a comparison, a range, a type or alternatives with | stand for a part:
pattern Adult = {age: >= 18}
{age: 20} is Adult // yes
match {age: 30} { {age: 18..65} => "working age"; _ => "?" } // working age
match [3, "b"] { [int, x] => x; _ => "no" } // b
match {code: 302} { {code: 301 | 302} => "moved"; _ => "other" } // moved
Schemas
A named map pattern of types is a schema; a field written name?: may be missing:
pattern Config = {port: int, host: text, debug?: bool}
json('{"port": 8080, "host": "localhost"}') is Config // yes
{port: "80", host: "x"} is Config // no
{port: 80, host: "x", debug: 3} is Config // no
is tests strictly; as converts field by field where as converts, or fails naming the field:
{port: "80", host: "x"} as Config // {port:80 host:"x"}
{port: "abc", host: "x"} as Config // error: port of Config takes an int, got "abc"
At any depth: find
find matches a pattern at every depth of maps, pairs and lists and gives the list of what it bound:
page = {top: {logo: {type: "image", src: "logo.png"}}, body: [{type: "text"}, {type: "image", src: "cat.jpg"}]}
page find {type: "image", src} // ["logo.png" "cat.jpg"]
[1, 2], [3, [1, 5](/pannous/warp/wiki/1,-2],-[3,-[1,-5)] find [1, x] // [2 5]
Replace
replace … with … rewrites every match at any depth, the children first; the names bound by the pattern can be used
in the replacement:
[1, 2], [3, [1, 5](/pannous/warp/wiki/1,-2],-[3,-[1,-5)] replace [1, x] with x // [2 [3 5]]
page = {title: "Cats", body: [{type: "image", src: "cat.jpg"}]}
page replace {type: "image", src} with {type: "link", href: src} // {title:"Cats" body:[{type:"link" href:"cat.jpg"}]}
Code
In a data(…) pattern, $name is a hole that takes the code at its place; a hole used twice must match equal code:
simplify(code) := match code { data($a + 0) => a; data($a * 1) => a; _ => code }
simplify(data(n * 1)) // n
match data(g(y, y)) { data($f($x, $x)) => "twice"; _ => "no" } // twice
With replace, holes rewrite code: code replace data($a * 1) with a turns n * 1 + k * 1 into n + k.
Outside a pattern, data(…) with holes is a template: $a splices the code bound to a, $(expr) splices a computed
value, and $xs... takes, then splices, the remaining items. Holes match in arm keys and right of is or ==:
swap(c) := match c { data($a - $b) => data($b - $a); _ => c }
swap(data(x - 1)) // data(1 - x)
drop_first(c) := match c { data($f($first, $rest...)) => data($f($rest...)); _ => c }
drop_first(data(g(1, 2, 3))) // data(g(2, 3))
field = data(width)
data(m.pop($(text(field)))) // data(m.pop("width"))
In parameters
A parameter can be a pattern; its names take the parts of the argument:
add_pair([a, b]) := a + b
add_pair([3, 4]) // 7
first_of([a, ...rest]) := a
class Point{x:int; y:int}
total(Point{x, y}) := x + y
total(Point(2, 5)) // 7
An argument of another shape is the error "no case for …". Planned: map patterns, greet({name}) := …
(card param-map-pattern).
Not by definitions
A function is defined once. A second definition of the same shape is the error "fib is defined twice", whose fix joins the cases in a match:
fib(n) := match n { 0 => 0; 1 => 1; _ => fib(n - 1) + fib(n - 2) }
Overloads by parameter type are different shapes and stay allowed, see overloading.