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.