Slide 28 of 74

Matching vectors and strings

Vector patterns
proc main(): void {
	command := ["move", "north", "2"]
	if command is ["move", dir, ...steps] {
		echo "{dir}, {len(steps)} more"
	} else if command is ["stop"] {
		echo "halt"
	}
}
String templates, and where a hole stops
proc main(): void {
	if "/users/42/posts/7" is "/users/{id}/posts/{postId}" {
		echo "{id} / {postId}"            // 42 / 7
	}
	if "a/b/c" is "{x}/{y}" {
		echo "{x} | {y}"                  // a | b/c — the shortest first hole
	}
	if "/users/42/extra" is "/users/{id}" {
		echo id                           // 42/extra — a last hole takes the rest
	}
	if "/users/42/extra" is "/users/{id is (\d+)}" {
		echo "never: digits only, and the template covers the whole string"
	}
}
A hole that says what it takes
func route(path: Str): Str {
	// Only digits reach id, so /users/nobody falls through
	// to the next branch instead of matching this one.
	if path is "/users/{id is (\d+)}" {
		return "user #{id}"
	}
	// The two kinds of hole mix freely — they are one construct.
	if path is "/api/v{version is (\d+)}/{rest}" {
		return "v{version}, then {rest}"
	}
	// Braces inside the parentheses are the regex's own: this is
	// four digits, not an interpolation.
	if path is "/logs/{year is (\d{4})}" {
		return "logs for {year}"
	}
	return "no match"
}

proc main(): void {
	echo route("/users/42")        // user #42
	echo route("/users/nobody")    // no match
	echo route("/api/v2/things")   // v2, then things
	echo route("/logs/2026")       // logs for 2026
}
What a string pattern refuses
proc main(): void {
	path := "/users/42"

	// Each of these is refused before the program is built:
	// if path is "/users/{id}{name}" { }        // two open holes side by side
	// if path is "/users/{len(path)}" { }       // a hole binds, it does not compute
	// if path is "{a is (a{2,1})}" { }          // {2,1} counts down
	// if path is "{a is ((?=b)c)}" { }          // no lookaround
	// if path is "{a is ((b)\1)}" { }           // and no backreference

	// Fine: the first hole says where it ends.
	if path is "/users/{id is (\d+)}{rest}" {
		echo "{id}[{rest}]"
	}
}
Patterns inside patterns
type Route {
	Page { segments: Str[dyn] }
	Missing
}

func resolve(path: Str): Result<Route, Str> {
	if path == "" {
		return Result.Error("empty")
	}
	return Result.Ok(Route.Page(split(path, ",")))
}

proc main(): void {
	path := "foo/abc/bar,x,y"

	// A Result holding a Page, holding a vector whose first element
	// is a string with a hole in it: one pattern.
	if resolve(path) is Result.Ok(Route.Page(["foo/{padding}/bar", ...rest])) {
		echo "{padding}, then {len(rest)} more"      // abc, then 2 more
	}

	// The very same match, written out as the chain it abbreviates.
	if resolve(path) is Result.Ok(route) && route is Route.Page(parts) && parts is [head, ...rest] && head is "foo/{padding}/bar" {
		echo "{padding}, then {len(rest)} more"
	}

	// A literal stands where a name would: the field has to equal it.
	if resolve("") is Result.Error("empty") {
		echo "nothing to resolve"
	}
}

A vector pattern matches element by element. An element is a literal — an Int, Float, Str, Atom or Bool — a name that binds it, or a pattern of its own, and a last ...rest binds whatever follows as a vector. Without one, the lengths have to be equal.

A string pattern is a template whose {holes} bind the text between its literal pieces. The template covers the whole string, and each hole takes the shortest text that lets the rest match — so "a/b/c" is "{x}/{y}" gives x a and y b/c, and a hole at the end takes everything left: "/users/42/extra" is "/users/{id}" binds id to 42/extra. That is the routing trap a hole that says what it takes avoids: {id is (\d+)} binds only what the regular expression matches.

Nothing between those parentheses is escaped by the string — {year is (\d{4})} is four digits, not an interpolation of 4 — and the regular expression is read at compile time, so a malformed one is a compile error naming what is wrong. A pattern has to have one reading: two open holes may not sit side by side with no text between them, though a hole that says what it takes ends itself; and a hole is a plain name, since a pattern binds and does not compute. A string being built, rather than matched, cannot hold a regex hole.

Patterns nest. Wherever a pattern binds a name — a variant's field, a vector's element — it may hold a pattern instead, and the whole is still one match: r is Result.Ok(Route.Page(["home", ...rest])). It is exactly the && chain it abbreviates, outer value first, and compiles to the same program. A variant's field may also hold a literal it has to equal. One pattern binds a name once, and a variant pattern has to be of the type it is matched against; anything else is a compile error.