Destructuring and Pattern Matching
There are four places in Agency you can pattern match:
let/constdeclarations (binding only).- The
isoperator (boolean test, optionally with bindings inside anif/while). matchblock arms (value matching with bindings).forloop iteration variable.
Destructuring in declarations
Array destructuring
const items = [1, 2, 3, 4, 5]
const [a, b] = items // a = 1, b = 2
const [first, _, third] = items // skip the second element
const [head, ...rest] = items // head = 1, rest = [2,3,4,5]The rest binder must be the last element. You can't have [a, ...m, b] for example.
Object destructuring
const person = { name: "Bob", age: 30, city: "NY" }
const { name, age } = person // shorthand
const { name: n, age: ageRen } = person // rename
const { name, ...others } = person // others = { age: 30, city: "NY" }
const { coords: [x, y] } = nested // nested patternsWildcards and rest
_matches anything and binds nothing. Note that_foois still a legal identifier....restcollects the remaining elements (array) or properties (object).
The is operator
expr is pattern is a boolean test. In a pure-boolean context (e.g. as the right-hand side of an assignment) it returns true / false:
const isShow = step is { type: "showPolicy" } // booleanIn an if or while condition, shorthand binders inside the pattern introduce variables in the body:
if (step is { type: "showPolicy", policy }) {
print(policy.name) // policy is in scope here
}Shorthand binders in pure-boolean contexts (assignment value, return, function argument) are a compile error. Example:
const step = {
type: "showPolicy",
policy: "privacy"
}
// allowed
const isShow1 = step is { type: "showPolicy" }
const isShow2 = step is { type: "showPolicy", policy: _ }
const isShow3 = step is { type: "showPolicy", policy: "privacy" }
// not allowed
const isShow4 = step is { type: "showPolicy", policy }In that last one, it looks like you are trying to create a new policy variable, which is not allowed. If you don't care what the value of the policy field is, just don't include it.
Match blocks
Match blocks support literal arms:
const status: "success" | "failure" | "pending" = getStatus()
match (status) {
"success" => print("Yay!")
"failure" => print("Boo!")
"pending" => print("Waiting…")
}They also accept object and array patterns:
match (event) {
// you can now use the `x` and `y` variables in the arm body
{ type: "click", x, y } => handleClick(x, y)
{ type: "scroll", delta } => handleScroll(delta)
_ => ignore()
}A guard clause if (…) can be appended to any arm:
match (request) {
{ kind: "user", age } if (age >= 18) => allow()
{ kind: "user" } => block()
_ => unknown()
}match(expr is pattern) form
When you want to destructure once and then dispatch on guards, write:
match (req is { user, role }) {
role == "admin" => grantAll(user)
role == "editor" => grantEditing(user)
_ => grantReadOnly(user)
}Match expressions
You can also assign the result of a match to a variable, or return it from a function. This is called a match expression.
Implicit returns
A single-expression arm returns its value implicitly:
const points = match(grade) {
"A" => 100
"B" => 80
_ => 0
}An arm can also be a block of statements. Block arms must explicitly return a value:
const val = match(result) {
success(v) => {
print(v)
return v * 2
}
failure(e) => e.message
}When you assign a match to a variable, every arm must return a value, and you can't have a bare return (return without a value).
return returns from the match, not the function
Inside a match arm, return expr returns from the match, not from the enclosing function. If you want to return from the function, put return in front of the match expression instead:
def classify(r: Result<number>): string {
return match(r) {
success(v) => "got ${v}"
failure(e) => "err: ${e}"
}
}Returning an object literal
Just like in JavaScript, if you want to implicitly return an object literal from a single-expression arm, you need to wrap it in parentheses.
kind => ({ label: kind })Or just use the block form, which doesn't require parentheses:
kind => { return { label: kind } }What type does a match expression have?
There are two cases, depending on whether you tell Agency the type up front.
You annotated the variable. Every arm has to produce that type. If an arm produces something else, that's an error:
const label: string = match(grade) {
"A" => "top"
"B" => "good"
_ => 0 // error: expected string, got number
}You didn't annotate the variable. Agency figures out the type for you: it's the union of whatever the arms produce. Here size is a string:
const size = match(n) {
100 => "big"
_ => "small"
}A match expression must always produce a value
When you use match as an expression, it has to hand back a value. That means every case has to be covered — otherwise there'd be a path where the match produces nothing.
type Shape = { kind: "circle", r: number }
| { kind: "square", side: number }
// error: not exhaustive, missing `{ kind: "square" }`
const area = match(shape) {
{ kind: "circle", r } => 3.14 * r * r
}To fix it, cover every case or add a _ catch-all:
const area = match(shape) {
{ kind: "circle", r } => 3.14 * r * r
{ kind: "square", side } => side * side
}This is an exhaustiveness check.
Open types: always add a _ arm
Agency can do an exhaustiveness check whenever the scrutinee (the thing being matched) has a closed type, like a Result, a literal union like "a" | "b", a boolean.
Some types are open — a bare string or number, for example. Agency can't list every possible string, so it can't tell you when you've missed one. That means it won't warn you, but at runtime a value that matches no arm makes the whole match produce undefined:
const greeting = match(name) { // name is a plain string
"Ada" => "Hi Ada!"
"Alan" => "Hi Alan!"
}
// greeting is undefined if name is "Grace"So whenever the scrutinee is an open type, add a _ arm to be safe:
const greeting = match(name) {
"Ada" => "Hi Ada!"
"Alan" => "Hi Alan!"
_ => "Hello!"
}v1 restrictions
Here are the places you can't use match blocks right now:
- Inside
parallel,seq,thread, andsubthreadblocks.
You can't use goto with match blocks:
// not allowed
goto match(x) {
"next" => next
_ => end
}Result patterns
The success and failure keywords work as patterns for ergonomic Result type unwrapping.
Boolean test
const worked = result is success
const failed = result is failureBinding in if/while
if (result is success(value)) {
print(value) // value is the unwrapped success value
}
if (result is failure(err)) {
print(err) // err is the error string
}In match blocks
match (result) {
success(v) => print("Got: ${v}")
failure(e) => print("Error: ${e}")
}Combined with match(expr is pattern) form
match (result is success(v)) {
v > 0 => print("positive")
_ => print("zero or negative")
}Nested inside other patterns
Result patterns may appear as nested elements inside array or object match patterns:
match (pair) {
[success(v), _] => print("first ok: ${v}")
[failure(e), _] => print("first err: ${e}")
_ => print("other")
}The bound value (v above) is narrowed precisely — it has the success value's type, so it can be used wherever that type is required (e.g. passed to a function expecting a number), with no extra guard.
Note: failure(e) binds only the error string. For checkpoint, functionName, or args, use the traditional if (isFailure(result)) form and access fields on the result variable directly.
For loop destructuring
The iteration variable can be an array or object pattern:
for ([key, value] in entries) {
print(key, value)
}
for ({ name, age } in users) {
print(name, age)
}Failure semantics
Destructuring relies on the underlying JavaScript runtime: reading a property of null or undefined (e.g. const { name } = null) throws a TypeError, which Agency captures and surfaces as a failure Result. At runtime, a match without a _ arm that matches no other arm is a no-op — no branch runs.