Patterns
Take records and case payloads apart by name in match arms, parameters and declarations, with one pattern grammar.
A pattern takes a value apart by naming its parts. It looks like JavaScript’s object destructuring: constant { x, y } = point declares a constant for each member it names. The same braces work in a match arm, in a case payload (the record a case carries), in a parameter and in a declaration. What a pattern saves you is naming the record and then reaching back into it, member by member.
implementation {
constant point = { x = 3, y = 4, label = "corner" }
constant { x, y as row } = point
Terminal.print(x) § 3
Terminal.print(row) § 4
}
{ x, y as row } stands where the declaration’s name would. Each member it names becomes a binding: a name declared from one part of the value. x binds the member x. y as row binds the member y under the name row. The pattern leaves label out, and that is allowed: a pattern names only the members it needs.
Compared with JavaScript.
const point = { x: 3, y: 4, label: "corner" }
const { x, y: row } = point
Three habits mislead here. x = 0 inside a pattern checks that the member holds 0. It is not a default, and only a match arm or a test’s require may check a value. x: Integer gives the member’s type, not a new name; Essence renames with as. And there are no array patterns, so [first, second] cannot stand where a name goes. More on Coming from JavaScript › Bindings.
A pattern has no defaults, because a record holds every member its type names. A default goes on a parameter, after its type: _ size: Size = { width = 1, height = 1 }. A member that may be absent is an Optional. Bind it, then read it with value(defaultingTo:): discount::value(defaultingTo 0) returns the item, or 0 for #Empty.
One grammar
A pattern stands in six positions: a match arm’s matcher (the part after case), a case payload, a function’s parameter, a function literal’s parameter, a declaration, and a test’s require. The members inside a pattern’s braces are written the same way in every position.
implementation {
type Size = { width: Integer, height: Integer }
function area(of { width, height }: Size) -> Integer {
<- width::multiply(with height)
}
constant hall: Size = { width = 3, height = 4 }
constant { width as across, height } = hall
constant labels = [hall, { width = 5, height = 2 }]
::map(({ width, height } as room) {
<- "{width} by {height}, area {area(of room)}"
})
Terminal.print(across) § 3
Terminal.print(height) § 4
Terminal.print(labels) § ["3 by 4, area 12", "5 by 2, area 10"]
}
The same { width, height } takes a Size apart in area’s parameter, in the function literal’s parameter and, with a rename, in a declaration. Each member of a pattern is one of these forms:
| Form | Binds | Checks | Example |
|---|---|---|---|
name |
name |
nothing | { key } |
name: Type |
name |
the member’s type | { x: Integer } |
name = value |
nothing | the member’s value | { x = 0, y } |
name as other |
other |
nothing | { y as row } |
name: Type as other |
other |
the member’s type | { x: Integer as column } |
name = value as other |
— | — | a syntax error: Expected '}' but found 'as'. |
name as { … } |
what the inner pattern binds | what the inner pattern checks | { start as { x, y } } |
{ … } as whole |
the parts, and the whole value as whole |
what the parts check | { width, height } as size |
A bare name is name: Type with the type left off. The two bind the same name and differ only in what they check. In a match arm, name: Type is a check: the arm runs only when the member has that type. In a declaration or a parameter it is an annotation, which the compiler checks like any other.
name = value binds nothing, because the value is written right there. It checks whether the member holds that value. Such a check cannot be renamed either, which is why x = 0 as zero is a syntax-error.
as renames. It is not TypeScript’s as type assertion: it gives a member a new name and changes no type. After a member’s name, as { … } nests a second pattern for that member’s value. The part after as is a binder, the thing that receives the value. A binder is a name or another pattern. After a closing brace, as whole binds the whole value beside its parts.
Each form is allowed in all six positions, with two exceptions. name = value works only in a match arm, case payloads included, and in a test’s require. A match arm’s own record pattern takes no as whole.
A test’s require is written require PATTERN = EXPR, and Tests teaches it. A failed require ends the test, which is why its pattern may check values.
In a match arm
implementation {
type Click = { x: Integer, y: Integer }
type KeyPress = { key: String }
constant x = 99
function describe(_ input: Click | KeyPress) -> String {
<- match input -> String {
case { x = 0, y = 0 } { <- "the origin" }
case { x, y } where x::is(y) { <- "on the diagonal at {x}" }
case { x as column, y as row } {
<- "{column}/{row}, and x is still {x}"
}
case { key } { <- "pressed {key}" }
}
}
Terminal.print(describe({ x = 0, y = 0 })) § the origin
Terminal.print(describe({ x = 3, y = 3 })) § on the diagonal at 3
Terminal.print(describe({ x = 3, y = 4 })) § 3/4, and x is still 99
Terminal.print(describe({ key = "Enter" })) § pressed Enter
}
A record pattern is a matcher: a case arm runs when the subject, the value being matched, fits its pattern. The subject here is a union, Click | KeyPress. Each pattern picks out one type of the union by the members it names, without naming the type. To take a single record apart, use a declaration.
A guard, the where condition after a matcher, sees the bindings. where x::is(y) reads the x and y that the pattern bound.
A binding shadows a name from outside the arm, as x in the second arm shadows the constant x. as binds a member under another name and leaves the outer one readable. That is how the third arm prints 99.
Inside a record-pattern arm, @ is the subject narrowed to the pattern’s own shape: the members the pattern named, and no others. An arm whose pattern names only x cannot read @.y, and the error is unknown-member:
[unknown-member]
Error: { x: Integer } has no member 'y'
╭─┤ input.es:7:45 │
│
7 │ case { x = 0 } { <- "on the y axis at {@.y}" }
│ ▲
│ ╰── no such member
│
│ Note: { x: Integer } has 'x'.
───╯Name y in the pattern, case { x = 0, y }, and read it as y.
@ already names the whole value, so an arm’s own pattern takes no as whole. case { x, y } as click is redundant-pattern-binder, and its Help says to write @ where click was meant.
A pattern may name a member only some of the union’s types have, which is how an arm tells them apart. A member none of them declares is read by how it is written. A record may carry members its type leaves out, so a member written with its type, as in case { label: String }, is a test the value may pass, and label is a String in the arm. A bare member none of them declares is taken for a misspelling. Over a Click | KeyPress, the arm case { kye } is unknown-member at kye, with the Help Did you mean 'key'?, and a second Help that offers writing its type.
In a case payload
implementation {
choice Shape {
Rectangle { width: Integer, height: Integer },
Square { side: Integer },
Empty,
}
function area(of shape: Shape) -> Integer {
<- match shape -> Integer {
case #Rectangle({ width, height }) {
<- width::multiply(with height)
}
case #Square({ side }) { <- side::multiply(with side) }
case #Empty { <- 0 }
}
}
function describe(_ shape: Shape) -> String {
<- match shape -> String {
case #Rectangle({ width = 0 }) { <- "a flat rectangle" }
case #Rectangle({ width, height } as box) {
<- "{width} by {height}, area {area(of box)}"
}
case #Square({ side } as square) {
<- "side {side}, area {area(of square)}"
}
case #Empty { <- "nothing" }
}
}
Terminal.print(describe(#Rectangle({ width = 3, height = 4 }))) § 3 by 4, area 12
Terminal.print(describe(#Rectangle({ width = 0, height = 4 }))) § a flat rectangle
Terminal.print(describe(#Square({ side = 2 }))) § side 2, area 4
}
A case matcher’s binder, the part in parentheses after the case name, may be a pattern. #Rectangle({ width, height }) takes the payload apart and binds its members. A payload pattern is partial and may check values, as in any other matcher: #Rectangle({ width = 0 }) matches only a rectangle whose width is 0, and binds nothing.
{ width, height } as box binds the parts and the whole. For a payload record, the whole is the case value: box is a Shape#Rectangle, the type @ has in that arm. It passes where a Shape is wanted, as in area(of box). A function that takes { width: Integer, height: Integer } refuses it with argument-type-mismatch, labelled this is a Shape#Rectangle. To pass such a function a record, build one from the bindings: { width, height }.
A payload is a record too, so it may carry a member its case leaves out. #Rectangle({ tag: String }) tests for one and binds tag as a String, while a bare #Rectangle({ tag }) is unknown-member. On a case with one member, the pattern is read against that member’s value instead, as the next example shows.
On a case with one member, the binder may be a single name, because the case’s constructor takes that member’s value:
implementation {
constant scores = [7, 0, 12]
function describe(_ score: Optional<Integer>) -> String {
<- match score -> String {
case #Value({ item = 0 }) { <- "a zero" }
case #Value(item) { <- "a score of {item}" }
case #Empty { <- "no score" }
}
}
Terminal.print(describe(scores::firstItem())) § a score of 7
Terminal.print(describe(scores::item(at 1))) § a zero
Terminal.print(describe(scores::item(at 5))) § no score
}
A single name binds that member’s value under the name you choose. #Value’s one member is called item, so #Value(item) means #Value({ item }), and #Value(score) means #Value({ item as score }). To check the value, spell the member out: #Value({ item = 0 }).
When the one member is itself a record, a payload pattern has two readings:
implementation {
type Tally = { index: Integer, total: Integer }
choice Progress {
Going { state: Tally },
Finished { total: Integer },
}
function describe(_ progress: Progress) -> String {
<- match progress -> String {
case #Going({ index } as tally) {
<- "step {index}, {tally.total} so far"
}
case #Finished(total) { <- "done at {total}" }
}
}
function remaining(_ progress: Progress, of goal: Integer) -> Integer {
<- match progress -> Integer {
case #Going({ state as { total } }) { <- goal::subtract(total) }
case #Finished { <- 0 }
}
}
constant now: Progress = #Going({ state = { index = 2, total = 7 } })
Terminal.print(describe(now)) § step 2, 7 so far
Terminal.print(remaining(now, of 10)) § 3
}
#Going({ index } as tally) reads the member’s value, a Tally, and takes it apart. #Going({ state as { total } }) reads the payload record, whose one member is state, and nests a pattern for it. When the member’s value has a member with the same name as the case’s own member, the payload record wins.
The as binder follows the reading. { index } as tally reads the member’s value, so tally is a Tally, and tally.total reads its total.
A single name works only on a case that carries one value. #Rectangle carries two, so case #Rectangle(box) is unbindable-case-payload:
[unbindable-case-payload]
Error: 'Shape#Rectangle' has no single value to bind
╭─┤ shapes.es:10:20 │
│
10 │ case #Rectangle(box) { <- "a rectangle" }
│ ─┬─
│ ╰─── this Case carries 2 values
│
│ Help: Take the payload apart instead: '#Rectangle({ width, height })'.
│
│ Note: Its values are 'width', 'height'.
────╯In a parameter
implementation {
type Size = { width: Integer, height: Integer }
function area(of { width, height }: Size) -> Integer {
<- width::multiply(with height)
}
function describe(
_ { width, height } as size: Size = { width = 1, height = 1 },
) -> String {
<- "{width} by {height}, area {area(of size)}"
}
Terminal.print(area(of { width = 6, height = 7 })) § 42
Terminal.print(describe({ width = 2, height = 3 })) § 2 by 3, area 6
Terminal.print(describe()) § 1 by 1, area 1
}
A function’s parameter takes a pattern where the name the body reads would go. The label stays in front of it. area is called area(of { width = 6, height = 7 }) whatever its body does with the argument. Functions covers labels. describe has no label, binds the whole as size, and carries a default value after its type, which describe() uses.
Compared with JavaScript.
type Size = { width: number; height: number }
function area({ width, height }: Size): number {
return width * height
}
area({ width: 6, height: 7 }) // 42
In both languages the pattern replaces the parameter’s name. Essence keeps the label in front of it, so a call does not change when the body starts taking its argument apart. More on Coming from JavaScript › Functions.
The same pattern works on a method’s or a static’s parameter, which Namespaces and methods shows.
A default cannot read a name that a pattern binds, even one in an earlier parameter, which JavaScript allows. Such a default is default-references-pattern-binding:
[default-references-pattern-binding]
Error: 'width' is bound by a Pattern and can not be read by a default
╭─┤ size.es:4:62 │
│
4 │ function frame(_ { width, height }: Size, border: Integer = width) -> Integer {
│ ──┬── ──┬──
│ ╰────────────────────────────────────────│──── bound by this Pattern
│ │
│ ╰──── read here
│
│ Help: Read the member off the Parameter itself, or write the value out.
│
│ Note: A Pattern's bindings are Constants at the head of the body, and every default is worked out before the body runs.
───╯Give the parameter a plain name, _ size: Size, and the default may read size.width.
In a function literal
implementation {
constant names = ["Ada", "Grace"]
constant born = [1815, 1906]
constant lines = names
::pair(with born)
::map(({ first, second }) { <- "{first} ({second})" })
Terminal.print(lines) § ["Ada (1815)", "Grace (1906)"]
constant sum = loop(
startingWith { index = 1, total = 0 },
step ({ index, total } as state) {
if index::isGreaterThan(5) {
<- #Done(total)
}
<- #Continue({
state with
index = index::add(1),
total = total::add(index),
})
},
)
Terminal.print(sum) § 15
}
A function literal’s parameter takes a pattern too. pair(with:) returns a list of { first, second } records, and ({ first, second }) reads each one. Essence has no tuples: a method that returns two values returns a record, and a pattern is how you read it.
step ({ index, total } as state) takes apart the state that loop(startingWith:step:) passes to each step. It keeps the whole as state for the with that builds the next one. Iteration covers loop.
A literal that writes its parameter’s type puts the type after the pattern, the way a function does: (_ { width, height }: Size) -> Integer { … }.
In a declaration
implementation {
type Size = { width: Integer, height: Integer }
constant numbers = [1, 2, 3, 4, 5]
constant { accepted, refused } = numbers::partition(where (n) {
<- n::isEven()
})
Terminal.print(accepted) § [2, 4]
Terminal.print(refused) § [1, 3, 5]
constant line = { start = { x = 1, y = 2 }, end = { x = 4, y = 6 } }
constant { start as { x as left }, end as { x as right } } = line
Terminal.print(right::subtract(left)) § 3
constant { width, height } as size: Size = { width = 2, height = 5 }
Terminal.print("{width} by {height}: {size}") § 2 by 5: { width = 2, height = 5 }
variable { index, total } = { index = 1, total = 0 }
index = index::add(1)
total = total::add(index)
Terminal.print("{index} {total}") § 2 2
}
A constant or variable declaration takes a pattern where its name goes. partition(where:) returns { accepted, refused }, and one declaration reads both lists.
start as { x as left } nests. It reads start, then the x inside it, and binds that as left. TypeScript nests with a colon, { start: { x: left } }. Here a colon gives a type, so nesting takes as.
as size binds the whole value beside its parts. An annotation follows the pattern, after as whole when there is one: { width, height } as size: Size.
The keyword applies to every binding: variable { index, total } declares two variables, and each is assigned on its own.
Patterns that cannot decline
A pattern that can fail to match is refutable. A value check makes it so: width = 0 fails on any other width. When a match arm’s pattern fails, the next arm is tried. A declaration and a parameter have no next arm, so they take only irrefutable patterns, the kind that cannot fail. A value check in either, at any depth, is refutable-pattern:
[refutable-pattern]
Error: A Declaration can not match a value against a Pattern
╭─┤ size.es:4:13 │
│
4 │ constant { width = 0, height } = size
│ ────┬────
│ ╰────── this can decline the value
│
│ Help: Write only 'width' to bind it, and ask about its value with 'match'.
│
│ Note: A Matcher may constrain a member by value, because an arm that declines falls through to the next one. A Declaration has nowhere to fall through to.
───╯A parameter gets the same code, with A Parameter can not match a value against a Pattern. Ask the question in a match, and declare from what its arms return. If you meant width = 0 as a JavaScript default, write width alone: a Size always holds its width.
A type check does not make a pattern refutable. In a declaration, { width: String } is an annotation, and a value that does not fit is a type error. constant { width: String } = size is assignment-type-mismatch: This value does not fit the declared Type of Constant 'width'.
A declaration also cannot take a union apart, even when every type in the union has the member you name. A union has no members to read, so the pattern is type-without-members. Here Click has an x, and the help names the arm that reads it:
[type-without-members]
Error: This value has no members to look up
╭─┤ input.es:7:13 │
│
7 │ constant { x } = input
│ ▲
│ ╰── this is a Click | KeyPress
│
│ Help: Read '@.x' in a Match arm that narrows the value — 'case Click { … }'.
│
│ Note: Only Records, Cases and Namespaces have members.
───╯Take a union apart with match, where each arm names the members it expects.
Two more errors meet a pattern in a declaration:
| Code | Written | The compiler says |
|---|---|---|
duplicate-variable |
constant { width as measure, height as measure } = size |
Variable ‘measure’ is already declared |
unknown-member |
constant { widht, height } = size |
Size has no member ‘widht’, with the Help “Did you mean ‘width’?” |
What is not a pattern
Three places look like a pattern and are not one.
- The key list after
with. Afterwith, a bare name is a record to merge, so{ base with x }merges the recordxintobase. It is not shorthand forx = x. That shorthand lives in a record literal,{ x, y }, which builds a record from names the way a pattern takes one apart into names. - A property test’s
for anyparameter. A property test runs its body on generated values, and names them afterfor any. That parameter is a name and a type, never a pattern.for any ({ team, points }: Standing)isproperty-parameters, whose Help says to name the whole value and take it apart in the body. - A protocol requirement. A requirement is a signature a protocol asks each conforming type to fill in, with no body of its own. It has nowhere to bind names, so a pattern in its signature is
pattern-without-body. Protocols covers requirements. A provided method is written with a body, so its parameters take patterns as any method’s do.
Several forms from other languages do not exist, and each is a syntax-error. The one a JavaScript habit meets first is the array pattern:
[syntax-error]
Error: Expected an Identifier but found '['.
╭─┤ scores.es:4:11 │
│
4 │ constant [first, second] = scores
│ ▲
│ ╰── here
───╯Take a list apart with its methods, such as firstItem() and partition(at:), which Lists covers. The other forms:
| Written | The compiler says | Write instead |
|---|---|---|
case [first, second] |
Expected a Type but found '['. |
a list method, such as firstItem() |
constant { x, ...others } = point |
Expected an Identifier but found '.'. |
the members you need; the pattern ignores the rest |
constant { start: { x } } = line |
Expected ':' but found '}'. |
constant { start as { x } } = line |
case #Value(_) |
Expected an Identifier but found '_'. |
case #Value, with no binder |
case #Value(0) |
Expected an Identifier but found '0'. |
case #Value({ item = 0 }) |
case { item, discount as #Value(amount) } |
Expected '}' but found 'as'. |
a second match, below |
case #Value(item as count) |
Expected ')' but found 'as'. |
case #Value(count) |
case String as text |
Expected '{' but found 'as'. |
case String, reading the value as @ |
constant { width: Integer = 0, height } = size |
Expected '}' but found '='. |
a default on a parameter |
A record parameter’s default may fill in only some of its members, as the Records page shows.
A case matcher cannot sit inside a record pattern. Bind the member, and match it in the arm:
implementation {
type Order = { item: String, discount: Optional<Integer> }
type Note = { text: String }
function describe(_ entry: Order | Note) -> String {
<- match entry -> String {
case { item, discount } {
<- match discount -> String {
case #Value(amount) { <- "{item}, {amount} off" }
case #Empty { <- item }
}
}
case { text } { <- text }
}
}
constant lamp: Order = { item = "lamp", discount = #Value(5) }
constant desk: Order = { item = "desk", discount = #Empty }
Terminal.print(describe(lamp)) § lamp, 5 off
Terminal.print(describe(desk)) § desk
Terminal.print(describe({ text = "call back" })) § call back
}
The inner match takes discount apart, and still sees the item the outer arm bound.