Patterns of your own: using
On this page
- A function as a pattern
- What a binding gets
- Tried top to bottom, called at most once
- A pattern never advances anything
- Joining patterns, guards and columns
- A match with a function's pattern ends in else
- Outside a match: if, elif and a declaration that leaves
- How a match is compiled: jump tables
- Keeping the matched value: match (m = ...)
You need a case that no literal, range or comparison can say. Does the line start with GET, and what follows it? Is the number prime? Does the text begin with a number, and where does the number end? A chain of ifs can ask, but loses the match's table of cases. Instead, any ordinary function can be a pattern, written in a case with using. This recipe has the full rules, then how a match is compiled, and a way to keep the matched value for the cases. It assumes match from Conditionals and Option from Dict. Here a function adds up a sum written as text, with a match deciding what the text that is left starts with:
mod sums;
import stdio use *;
// Each pattern takes the text that is left as its first parameter
// The text after p, if the text starts with it
fn lit(text &str, p &str) Option[&str] {
if text.startsWith(p) { Some(&text[p.len() ..]); } else { None[&str]; }
}
// The text after a run of digits, if it starts with one
fn digits(text &str) Option[&str] {
var i = 0usize;
while i < text.len() and text[i] >= '0' and text[i] <= '9' { i += 1; }
if i > 0 { Some(&text[i ..]); } else { None[&str]; }
}
// True when nothing is left
fn atEnd(text &str) bool { text.len() == 0; }
// The text with its leading spaces skipped (not a pattern: it always succeeds)
fn blank(text &str) &str {
var i = 0usize;
while i < text.len() and text[i] == ' ' { i += 1; }
&text[i ..];
}
// The number written at the start of text, which ends where after begins
fn value(text &str, after &str) i64 {
var n = 0i64;
var i = 0usize;
while i < text.len() - after.len() {
n = n * 10 + i64.from(text[i] - '0');
i += 1;
}
n;
}
// Adds up a sum such as "12 + 30 - 5", or gives None if it is not one
fn sum(text &str) Option[i64] {
var rest = blank(text);
var total = 0i64;
var sign = 1i64; // the sign of the number that is due
while true {
match rest {
case let after using digits() { total += sign * value(rest, after); rest = blank(after); }
else { return None[i64]; }
}
match rest {
case let p using lit("+") { sign = 1; rest = blank(p); }
case let p using lit("-") { sign = -1; rest = blank(p); }
case using atEnd() { return Some(total); }
else { return None[i64]; }
}
}
None[i64];
}
fn main() {
each text in ["12 + 30 - 5", " 7", "1 + + 2", "100 - 1 x"] {
match sum(*text) {
case let n Some { print <- "\"", *text, "\" is ", n, "\n"; }
case is None { print <- "\"", *text, "\" is not a sum\n"; }
}
}
}
It prints:
"12 + 30 - 5" is 37 " 7" is 7 "1 + + 2" is not a sum "100 - 1 x" is not a sum
Try this: add a case case let p using lit("plus") { sign = 1; rest = blank(p); } to the second match, and give sum the text "2 plus 2".
Each case of the second match calls a function on the text that is left, and the first that says yes runs. digits and lit hand back the text after what they found, and the case moves rest on past it; atEnd only tests. That is how a parser can be written as a match over the text that is left, a case for each thing the text may start with.
A function as a pattern#
A pattern of your own is an ordinary function, or a method, that tests the matched value, and nothing about it is special to the language. Nothing is declared to make a function a pattern, and a library's patterns are written and used the same way as your own.
A case names it with using and the call of the function, written without the matched value: the matched value is the function's first argument, and, for a method, the argument after self. The arguments written are the ones that follow, so case using between(10, 20) calls between(n, 10, 20), and case using c.big() calls c.big(n). What the function gives back says what the pattern does:
- a bool only tests. case using atEnd() calls atEnd(rest), and the case is taken when it is true;
- an Option tests and binds. case let p using lit("+") calls lit(rest, "+"), and the case is taken when it gives Some, p being what it holds. The name comes first, as in case let n Ok, and may be var, so the case may change its own copy.
A function with several outcomes gives an enum instead, and is called once and matched as any enum is, with a case for each variant and every variant covered (Enums).
What a binding gets#
A binding gets what the function gave back, as a variant's binding does: the contents of the Some, a copy of plain data, or a move of what owns something, which is finalized as the case ends, as any bound value is. A pattern with several results gives them as a struct, bound whole and read by field name, or as a tuple, read by position:
struct Halves { pub left i32; pub right i32; }
// The tens and the units of a number of two digits or more
fn split(n i32) Option[Halves] {
if n >= 10 { Some(new Halves(n / 10, n % 10)); } else { None[Halves]; }
}
// The quotient and remainder, unless d is 0
fn divmod(n i32, d i32) Option[(i32, i32)] {
if d != 0 { Some((n / d, n % d)); } else { None[(i32, i32)]; }
}
match 47 {
case let m using split() { print <- m.left, " and ", m.right, "\n"; } // 4 and 7
else { }
}
match 47 {
case let q using divmod(5) { print <- q.0, " r ", q.1, "\n"; } // 9 r 2
else { }
}
And a pattern that hands out a value with a finalizer hands it to the case, which finalizes it as it ends:
struct Ticket {
pub seat i32;
fn final(self &uni) { print <- "ticket ", seat, " torn up\n"; }
}
fn issue(n i32) Option[Ticket] {
if n > 0 { Some(new Ticket(n)); } else { None[Ticket]; }
}
match 12 {
case let t using issue() { print <- "seat ", t.seat, "\n"; }
else { }
}
print <- "after the match\n";
prints seat 12, then ticket 12 torn up, then after the match.
Tried top to bottom, called at most once#
Cases are tried from the top, and a function runs only when its case is reached, at most once for it, never ahead of time. A case of another kind before it that matches means it is never called, and the cases after it are tried when it does not match. So a method pattern may keep a state that its calls change, since each call is made as written, in this order:
struct Counter {
pub calls i32;
pub fn big(self &mut, n i32) bool { calls += 1; n > 100; }
}
var c = new Counter(0);
each n in [5, 500, 7] {
match *n {
case 7 { print <- "seven\n"; }
case using c.big() { print <- "big\n"; }
else { print <- "small\n"; }
}
}
print <- "big was called ", c.calls, " times\n";
It prints small, big and seven, then big was called 2 times: the 7 was taken by the case above, and big was never asked. What a pattern costs shows where it is declared, not at the case, and whether a pattern is inline is its author's choice.
A pattern never advances anything#
A pattern tests the matched value and gives back what is left of it, as lit does, and the case moves on, by assigning the rest (rest = blank(p); in the sum), as it would change any state. Nothing is half consumed by a pattern that did not match, since nothing moves until a case has matched.
Joining patterns, guards and columns#
A test that only answers bool may be joined to other patterns with or, and followed by an if guard (Conditionals), as any pattern is:
fn isOdd(n i32) bool { n % 2 == 1; }
fn isPrime(n i32) bool {
if n < 2 { return false; }
var d = 2;
while d * d <= n { if n % d == 0 { return false; } d += 1; }
true;
}
let k = match n {
case using isOdd() or using isPrime() { "odd or prime"; } // 2 is prime
else { "neither"; }
};
A case that binds stands alone, never joined to others with or, and may have a guard that uses the binding. The cases below are tried in turn, so a short path falls through to the second:
match line {
case let rest using lit("GET ") if rest.len() > 2 { print <- "long ", rest, "\n"; }
case let rest using lit("GET ") { print <- "short ", rest, "\n"; } // "GET /a" is short
else { }
}
A match a, b may test a column with a function, case using isOdd(), 4, but binds nothing in it: a binding there is refused (Error 1419: A pattern in a column of 'match a, b' does not bind a variable). The matched value of a case that binds may be a call, which is made once, as for every pattern.
A match with a function's pattern ends in else#
A match with a pattern of your own ends in else, whatever its other cases cover (Covering every value): which values a function matches, only running it says. Even a match on a bool with a case for true and one for false needs it, once one case is a using:
Error 1423: A match with a 'using' pattern needs an 'else': which values a function's pattern matches is
known only by running it. End the match with 'else {}', which may say that no other value can happen.
Outside a match: if, elif and a declaration that leaves#
The same patterns bind in an if, an elif, and a declaration that leaves when the pattern does not match, with the matched value after the =, as the variants' bindings do (Binding past an early exit). A function that only tests is simply the condition of the if, if isOdd(n) { ++count; }:
fn handle(line &str) i32 {
if let rest using lit("GET ") = line { print <- "get ", rest, "\n"; }
elif let other using lit("DEL ") = line { print <- "del ", other, "\n"; }
let path using lit("PUT ") = line else { return -1; }; // leaves unless it matches
print <- "put ", path, "\n";
0;
}
How a match is compiled: jump tables#
Nothing is written for this: it is what the cases mean, and it matters only when you tune a match that runs very often. A match on one integer, character or enum compiles to a single switch on that value in every build, which the back end makes a jump table, a binary search or a bit test as the cases lie. The cases that count are those before the first that does not test the value against constants: a constant (a literal or a named constant), a range, or of those, and, on an enum, a variant, a binding of one, and a trait that names variants (Trait patterns). Those cases are the switch. The rest, from the first case with an if guard, a comparison such as > 3, a text, a range too wide to list (more than 128 values), a pattern of your own, or from the else, is tried in order as the default; so a pattern of your own and the cases after it are tried in order, as written. A match that covers an enum with no else has its last case as the default. Cases are still tried top to bottom: a value two cases share belongs to the first.
fn kind(c char) i32 {
match c {
case 'a' or 'e' or 'i' or 'o' or 'u' { 1; } // these four cases are the switch
case 'b' .. 'd' { 2; }
case '0' .. '9' { 3; }
case ' ' { 4; }
case 'Ā' .. '\U0010ffff' { 5; } // too wide: tried after the switch, in order
else { 0; }
}
}
Whichever way it is compiled, the answers are the same: kind gives 1, 2, 3, 4, 5 and 0 for 'e', 'c', '7', ' ', 'Ā' and 'z'.
Keeping the matched value: match (m = ...)#
Any expression can follow match, and its value is what the cases are matched with. To keep that value for the cases, the expression may be an assignment to a variable declared before the match. Today it must be written in parentheses: match m = n * 2 { is refused (Error 1004: Expected '{' to start a block).
var m = 0;
each n in 1 .. 4 {
match (m = n * 2) {
case 6 { print <- "six, from ", n, "\n"; }
else { print <- "m is ", m, "\n"; }
}
}
It prints m is 2, m is 4, six, from 3 and m is 8.
You can now write any test a match needs as an ordinary function, have it bind what it finds, and read a parser as a table of what the text may start with. The sum above walked its text with patterns and a loop; the next recipe walks a sequence the other way round, writing the walk once and handing out its values: generators.