Expressions
The precedence table
Loosest at the top. Everything is left-associative except the lambda arrow.
| power | operators | |
|---|---|---|
| 5 | -> | right-associative, so x -> y -> x + y is a function answering a function |
| 6 | match, catch | a transformation of the thing to the left |
| 9 | ?? | |
| 10 | \|\| | |
| 20 | && | |
| 25 | is | |
| 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:
matchandcatchsit below every arithmetic operator, soa + b match …transforms the sum rather than justb, anda + b catch …guards the sum. They sit above the arrow, sox -> y match …gives the lambda a body that matches rather than matching on the lambda.??is looser than||, soa ?? b || creads asa ?? (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 + 3groups the way it reads.&,^and|sit above the comparisons, soa & b == cis 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 = vis 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.