slate

Expressions

The precedence table

Loosest at the top. Everything is left-associative except the lambda arrow.

poweroperators
5->right-associative, so x -> y -> x + y is a function answering a function
6match, catcha transformation of the thing to the left
9??
10\|\|
20&&
25is
30== != < <= > >=one level, because a chain of them is one comparison
35.. ..<non-associative
40\|
42^
44&
50+ -
60* / % << >>a shift binds like a multiplication
80++ -- (postfix), call, […], ., ?., with

Three placements are worth knowing because they decide what a line means:

  • match and catch sit below every arithmetic operator, so a + b match … transforms the sum rather than just b, and a + b catch … guards the sum. They sit above the arrow, so x -> y match … gives the lambda a body that matches rather than matching on the lambda.
  • ?? is looser than ||, so a ?? b || c reads as a ?? (b || c). A program that writes both usually means the defaulting to happen last. (JavaScript refuses to mix them without parentheses.)
  • A shift binds like a multiplication, not like C’s, so 1 << 2 + 3 groups the way it reads. &, ^ and | sit above the comparisons, so a & b == c is not C’s surprise either.

Prefix operators are -, !, ~, ++ and --, all binding tighter than any infix operator and looser than a call — -f(x) negates the result.

_ where a value goes is not an operator and is not in the table, because what it reaches is decided by the nearest enclosing argument, bracketed group or right-hand side rather than by a binding power: map(xs, _ * 2) is map(xs, n -> n * 2). See Functions.

Arithmetic and bitwise

+ - * / % over integers and reals; see Values for what / does between two integers. + on two strings concatenates.

| ^ & ~ and the shifts << >> work on 64-bit integers.

Comparison

Comparisons chain rather than associate, which is mathematics’ reading and sysl’s:

val n = 5

print(0 <= n < 10)          // `n` is evaluated once
true

== and != compare by value; see Values.

Logic

&& and || short-circuit and answer the operand that decided, not a boolean:

print(1 && 2, null || "x")
2 x

?? answers its left operand unless that is null:

print(false ?? "d")         // `false` is a value
print(null ?? "d")
false
d

is

is puts a pattern where a condition is wanted, using the same grammar a match arm does:

print(3 is number, 3 is not string, 3 is 1 | 3 | 5)
true true true

It sits between && and the comparisons, so a is P && b > 0 is two terms.

Ranges

A range is a value:

val xs = [1, 2, 3, 4]

print(xs[1..<3])            // exclusive
print(xs[1..2])             // inclusive
print("hello"[..2])         // an end left out is taken from what it is used on
print(xs[2..])
[2, 3]
[2, 3]
hel
[3, 4]

An end left out is taken from whatever the range is used on. Ranges do not associate, so a..b..c is refused. a..=b is refused by name, since a reader arriving from Rust writes it once.

Field and index

. reads a field, […] indexes an array, a string or an object.

?. guards its own link and not the rest of the chain, which is Kotlin’s rule:

val a = { b: { c: 1 } }

print(a?.b?.c)
print(a.missing?.c)         // `?.` on the link that may be absent
1
null

a?.b.c reads b off a or answers null, and then asks .c of whatever that was — so a nullish a faults at .c, and a?.b?.c is what the reader means. The rule is the one slate states everywhere about absence: it stops at the boundary it arose at, and one character quietly excusing every link after it is the opposite of that.

  • o?.m(a) does not evaluate its arguments where there is nothing to call the method on.
  • There is no a?.[i].
  • o?.f = v is refused: there is no answer to what writing into absence should do.

with

a with b answers a copy of a with b‘s fields written over it. The right-hand side may be a literal or any expression answering an object:

val base = { a: 1, b: 2 }

print(base with { b: 9 })
print(base with { c: 3 })
{a: 1, b: 9}
{a: 1, b: 2, c: 3}

It binds as tightly as a field selection, so a + b with { … } changes b rather than the sum.

There is no spread in a literal. { ...o, b: 2 } is o with { b: 2 } and [...xs, y] is concat(xs, [y]), both of which slate has, so a second spelling would buy nothing.

Spread in a call

f(...xs) is the one spread slate has, and it exists because a computed argument list had no spelling at all:

show(...parts) = join(parts, "-")
val xs = ["b", "c"]

print(show(...xs))
print(show("a", ...xs, "d"))        // in any order, any number of times
b-c
a-b-c-d

Spreading something that is not an array is a fault naming the spread rather than the call. The checker says nothing about a call that spreads, the argument count being a run-time fact.

match

match is postfix — a transformation of the thing to its left, as in Scala and sysl. It is an expression, so it stands where a value is wanted:

what(v) = v match
    { kind: "point", at: [0, 0] } -> "origin"
    [first, ...rest] -> "a list starting " + string(first)
    n @ number if n < 0 -> "a negative number"
    _ -> "something else"

print(what({ kind: "point", at: [0, 0] }))
print(what([9, 1]))
print(what(-4))
print(what(true))
origin
a list starting 9
a negative number
something else

See Patterns for what an arm may be written with, and for when the arms have to cover everything.

A subject matching no arm is a fault, as Scala’s MatchError is.

catch

The postfix form of fault handling is an expression too:

toPort(text)
    val n = number(text)

    if n == null then throw "that is not a port"

    n

val port = toPort("nonsense") catch e ->
    print(s"${e.message}, so using the default")
    8080

print(port)
that is not a port, so using the default
8080

Blocks

A block’s value is its trailing expression, so a lambda or a definition whose body is several statements answers the last one. return is for leaving early and nothing else.

Every loop is an expression too, and break is what gives it a value.

Search

Esc
to navigate to open Esc to close