Patterns
One grammar, used in four places: a match arm, an is test, a binding (val { a } = o), and a
parameter that takes its argument apart.
match
match is postfix — a transformation of the thing to its left. A guard runs after the pattern has
bound:
classify(v) = v match
{ kind: "point", at: [0, 0] } -> "origin"
{ kind: "point", at: [x, y] } if x == y -> "diagonal"
[first, ...rest] -> "a list starting " + string(first)
"sat" | "sun" -> "a weekend"
n @ number if n < 0 -> "a negative number"
_ -> "something else"
print(classify({ kind: "point", at: [0, 0] }))
print(classify({ kind: "point", at: [2, 2] }))
print(classify([9, 1]))
print(classify("sun"))
print(classify(-4))
print(classify(true))
origin
diagonal
a list starting 9
a weekend
a negative number
something else
Arms are tried in order. A subject matching no arm is a fault, as Scala’s MatchError is.
An arm’s body may be an expression or an indented block, and the inline then forms work inside one.
What binds and what tests
A bare name in a pattern tests for a kind where it names one, and binds otherwise. The kind words are:
null boolean integer real number string array object function
plus promise, generator, regex, shape, and the eight temporal words —
instant, duration, date, time, dateTime, zone, zoned, period. That is the whole list:
twenty-one words. Every type, class and data variant
declared in the file adds its own name to it — and one declared or imported under a kind word
shadows the kind, exactly as a local shadows any other name: with a class Set in the file,
is Set, x: Set and a Set arm all mean the class.
None of them is a keyword. val int = 3 and val date = readIt() are ordinary bindings; the words
are read this way in pattern position and nowhere else. What it costs is real: a match arm may no
longer bind a name spelled date or time.
Reaching for one of them afterwards names what happened instead of just saying “not defined”.
val [a, time] = [1, 2]
print(time)
`time` here tests for a `time` value rather than binding one
number is the one word that is not a kind, being the union of integer and real.
_ matches anything and binds nothing.
@
n @ pat tests and names at once:
tell(v) = v match
n @ number if n < 0 -> s"the negative number ${n}"
p @ { x: 0, y } -> s"${p} is on the y axis at ${y}"
_ -> "something else"
print(tell(-4))
print(tell({ x: 0, y: 3 }))
the negative number -4
{x: 0, y: 3} is on the y axis at 3
A broad guard is why this matters. n if n < 0 binds anything, so the guard evaluates
[3, 4] < 0 and faults; n @ number if n < 0 cannot reach the guard with the wrong kind.
Ranges
1..100 matches a number between its ends, both included; ..< leaves the top one out, and either
end may be left off:
grade(n) = n match
0..59 -> "F"
60..<70 -> "D"
70..<80 -> "C"
80..<90 -> "B"
_ -> "A"
print(grade(12), grade(65), grade(95))
print(-4 is ..0, 4 is 3..)
F D A
true true
A step narrows a range pattern to every kth number, which is the by an expression writes and
means the same thing — so a declared type can say what an even digit is:
type Even = 0..<10 by 2
print(4 is Even, 5 is Even)
print(6 is 0..<10 by 2, 7 is 0..<10 by 2)
true false
true false
A step here is a whole number beside whole ends, and it counts from the end it walks away from — so
..100 by 2 is refused, having no end to count from.
Whole ends mean whole numbers, so 2.5 is 0..100 is false and 2.5 is 0.0..100.0 is true. A range
binds nothing, its ends being numbers, and it is a type in every position a pattern is one.
| and &
| is alternation. No alternative may bind a name, since it would be bound down one path and not
the other:
print("sun" match
"sat" | "sun" -> "a weekend"
_ -> "a working day")
a weekend
An alternative that binds is refused where it is written:
print(1 match
a | b -> 1
_ -> 2)
an alternative of a pattern may not bind a name
& is its dual, binds tighter, and may bind — every part of an intersection runs, so both names
are bound down the one path that matched:
both(v) = v match
{ a } & { b } -> s"$a $b"
_ -> "no"
print(both({ a: 1, b: 2 }), both({ a: 1 }))
1 2 no
Object patterns
An object pattern matches an object with at least those fields, because a record grows fields over its life.
type Note = { title: string, pinned?: boolean }
print({ title: "a" } is Note)
print({ title: "a", pinned: true } is Note)
print({ title: "a", pinned: 1 } is Note) // present, and does not fit
true
true
false
{ name: n } binds n, { name } is shorthand for { name: name }, and { pinned? } is a field
the subject need not have. An object literal reads { name } the other way round, as
{ name: name } built rather than taken apart; { pinned? } is a pattern’s alone, a literal writing
a field or not writing it.
A field a proto supplies counts, a pattern asking whether the value has the field — which is the
question . answers.
= is for a pattern that binds and ? is for one that tests, and each is refused where the other
belongs.
...rest gathers the fields the pattern did not name into a new object, TypeScript’s object rest.
It works wherever a pattern does — a binding, a parameter, a for head, a match arm and is — and
the pattern still matches partially, so with nothing left over rest is {}:
class Point
var x
var y
val o = { a: 1, b: 2, c: 3 }
val { b, ...rest } = o
print(b, rest)
val { title = "Untitled", ...others } = { size: 2 }
print(title, others)
val { x, ...left } = Point(1, 2)
print(x, left, left is Point)
describe({ kind, ...details }) = kind + " with " + string(keys(details))
print(describe({ kind: "box", w: 1, h: 2 }))
2 {a: 1, c: 3}
Untitled {size: 2}
1 {y: 2} false
box with ["w", "h"]
What rest holds is what keys walks: the object’s own fields, less
the ones the pattern named — a field a default stood in for included. A class or data value’s proto
and its tags are machinery and stay behind, and the copy is a plain object of no class. A rest is
a plain name and it comes last; anything written after it is refused:
val { ...rest, a } = { a: 1, b: 2 }
a rest element must be last in a destructuring pattern
Array patterns
An array pattern says how many elements the array has, and a ... is what opens it. So [a, b]
matches an array of exactly two and [a, ...rest] matches any array with at least one:
print([1, 2] is [integer, integer])
print([1, 2, 3] is [integer, integer])
print([1, 2, 3] is [integer, ...])
val [first, ...rest] = [1, 2, 3]
print(first, rest)
true
false
true
1 [2, 3]
This is the same reading a bracketed type has — the two are read by one grammar, so
type Pair = [string, integer] is a pair wherever its name is written.
A ... need not be named. [a, ...] is at least one element, and the rest are none of this
pattern’s business; nothing may follow it, there being no end to count back from.
Bindings
A val or a var may take its value apart:
val { title } = { title: "t", extra: 1 }
val [first, second] = [10, 20]
print(title, first, second)
t 10 20
is
is puts a pattern where a condition is wanted:
type Point = { x: number, y: number }
val v = 3
print(v is number, v is not string, v is 1 | 3 | 5)
print({ x: 1, y: 2 } is Point)
true true true
true
Exhaustiveness
Exhaustiveness is checked exactly where the value’s shape was written down, and nowhere else. slate is dynamically typed, so for an unannotated subject the set of values a name may hold is not known and nothing useful can be said.
What data adds is a closed list of variants, so a match over a subject annotated
with one is checked against it and every variant left out is named:
data Shape
Circle(r)
Rect(w, h)
Empty
sides(s: Shape) = s match
Circle(_) -> 0
Rect(_, _) -> 4
Empty -> 0
print(sides(Circle(1)), sides(Rect(1, 2)), sides(Empty))
0 4 0
A variant left out is named where the match is written:
data Shape
Circle(r)
Rect(w, h)
Empty
sides(s: Shape) = s match
Circle(_) -> 0
this match does not cover
A _ arm is how a program says it has finished listing.
Class and variant patterns
A class name or a data variant written with fields after it tests and takes apart in one breath — by position, or by name:
class Square
var side
class Circle
var radius
class Rect
var w
var h
tell(v) = v match
Square(n) -> s"a square of side ${n}"
Circle { radius: r } -> s"a circle of ${r}"
Rect { w, h } -> s"${w} by ${h}"
_ -> "something else"
print(tell(Square(3)))
print(tell(Circle(7)))
print(tell(Rect(2, 5)))
print(tell(42))
a square of side 3
a circle of 7
2 by 5
something else
The positional order is the constructor’s, and holds by construction: the field list a pattern is
checked against is the parameter list of the new the class was given.
Naming the class is also what lets a misspelled field be caught. A bare { raduis: r } is a legal
pattern that never matches — any object may lack any field — so the arm is silently dead. Written
after a class name there is a declaration to check it against, and it is refused where it stands.
Both forms nest, both take an @ binding, and the test is the proto walk — so a pattern written
for a base class takes an object of a class descended from it apart.
Types in patterns
A type name in pattern position is replaced by the pattern the type declared, while the
program is compiled:
type Point = { x: number, y: number }
print({ x: 3, y: 4 } is Point)
print({ x: 3 } is Point)
true
false
Nothing of a type’s structure exists at run time, so a type costs no instruction. A type may not
bind a name — type Tagged = { x: n } is refused — for the reason | may not.
A generic type is written here with the types it is given, and the substitution happens where the arm is written — so the pattern that runs is the ordinary one for the shape that came out:
type Pair[T] = [T, T]
sort(v) = v match
Pair[integer] -> "two integers"
Pair[string] -> "two strings"
_ -> "something else"
print(sort([1, 2]), sort(["a", "b"]), sort([1, "a"]))
two integers two strings something else
A type PARAMETER is a different thing and cannot be tested against. It is erased, so v is T
would be a bare name binding the value and matching everything — a test the reader believes and does
not get — and it is refused where it is written instead:
only[T](v) = v match
T -> "yes"
_ -> "no"
print(only(1))
`T` is a type parameter, and a type parameter is not there at run time