slate

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.

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.

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.

| 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.

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.

Array patterns

An array pattern tests the elements it writes and lets the rest through:

print(["a", 2] is [string, ...])

val [first, ...rest] = [1, 2, 3]

print(first, rest)
true
1 [2, 3]

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.

Search

Esc
to navigate to open Esc to close