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 |
| 30 | in | on the comparisons’ level and not in their chain |
| 35 | .. ..< | non-associative |
| 35 | by | the step of the range on its left |
| 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, and \ over integers alone. / answers a real whatever it
was handed and \ is the whole-number division; see Values for the pair and for what
each does by zero. + on two strings concatenates.
| ^ & ~ and the shifts << >> work on integers, of any width — they read a value as an endless
run of two’s-complement bits, so ~x is -x - 1, << grows and >> floors. See
Values.
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.
in
k in o is JavaScript’s in: whether o[k] would find a member. On an object that is a field
of its own or one up its proto chain, a property, or one of the universal methods; on an array, a
string or bytes it is a position inside it or a name the kind has; on any other kind, a name the kind
has. It reads nothing to answer, so a get is never called. Objects has has, which
asks about the object’s own fields only.
val base = { x: 1 }
val child = { proto: base, y: 2 }
print("y" in child, "x" in child, has(child, "x"), "z" in child)
print(0 in [7], 1 in [7], "length" in [7], 2 in "abc")
true true false false
true false true true
in sits with the comparisons, so "a" + b in o asks about the sum and k in o && ok is two
tests, but it does not join their chain: k in o == true compares the answer. null, a boolean
and a number have no members, and in refuses them where JavaScript throws a TypeError:
print("a" in 5)
integer has no members for `in` to ask about
A key of a kind the container is not indexed by is refused in the same way, and a position past the
end is simply not there. In a for head the first in is the loop’s own; one inside the iterable
is the operator.
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.
by gives a range a step
by takes the numbers the range covers down to every kth of them. It binds to the range on its
left — looser than .. and ..<, tighter than a comparison — so 0..<10 by 1 + 1 steps by two and
0..<10 by 2 == r compares the stepped range:
for x in 0..<10 by 2
print(x)
print(1..9 by 3, (1..9 by 3).length)
print([10, 20, 30, 40, 50, 60][0..<6 by 2])
0
2
4
6
8
1..9 by 3 3
[10, 30, 50]
A negative step runs the range downwards, and 10..0 by -1 is the countdown a for head wants:
var seen = []
for x in 10..7 by -1
push(seen, x)
print(seen)
print((0..10 by -1).length, (10..0 by 2).length)
[10, 9, 8, 7]
0 0
A step whose sign disagrees with the ends covers nothing — which is the answer 10..0 gave
before there were steps at all — so those two lengths are zero rather than a complaint.
A step of 0 is refused, where it is written if it is a number and where it is worked out if it
is not, since a range that never moves would never reach its end:
print(0..<10 by 0)
a range steps by 0
by is a soft word: it means a step only straight after a range, and is an ordinary name
everywhere else.
val by = 3
print(by * 2, (0..<12 by by).length)
6 4
A range whose top is left out takes no step — a word standing where the top belongs is read as that
top, so 0..by is a range up to what by holds.
Field and index
. reads a field, […] indexes an array, a string or an object.
?. guards the whole chain after it, which is JavaScript’s rule:
val a = { b: { c: { d: 1 } } }
val gone = { }
print(a?.b.c.d)
print(gone.missing?.b.c.d ?? "none")
1
none
The run of links is what the character guards, not the one link it was written on. Where the
left is null or absent, every ., […] and (…) after it is skipped — nothing along the rest of
the chain is worked out at all — and the expression answers null.
There are three guarded links, and each one opens a chain that reaches to the end of the run:
| written | guards | where the left is there |
|---|---|---|
o?.f | o | reads f off it |
xs?.[i] | xs | reads i out of it — the key is not worked out otherwise |
f?.(a) | f | calls it — the arguments are not worked out otherwise |
val holder = { xs: [10, 20], go: n -> n * 2 }
val empty = { }
val deep = { rows: [{ pick: () -> "here" }] }
print(holder.xs?.[1])
print(holder.go?.(21))
print(empty.xs?.[1] ?? "none")
print(empty.go?.(21) ?? "none")
print(deep?.rows[0].pick())
20
42
none
none
here
What a chain does NOT guard
A guard answers for the absence it was written about and for no other. A link after it that
finds a nullish value of its own faults there, exactly as it would with no ?. in the line:
val a = { b: null }
print(a?.b.c)
there is nothing here to read `c` from
a?.b?.c is what says both may be missing. So a chain is read link by link: each ?. marks one
place a value may not be there, and marks it once.
Brackets end a chain. (a?.b) is a chain that is over, so what follows the brackets asks the
answer it gave:
val a = null
print((a?.b).c)
there is nothing here to read `c` from
The rest of the rules
- A guard answers for an ABSENCE and never for a wrong kind.
f?.()wherefholds a3faults exactly asf()would; the character says this may not be here, not this may be anything. - A guarded call of a member keeps its receiver, as JavaScript’s does.
o.m?.(a)ando[k]?.(a)guard the member: where it is missing they answernullwith the arguments not worked out, and where it is there they call exactly whato.m(a)ando[k](a)call,ohanded over by the same rule.o?.m(a)ando?.[k](a)are the spellings that guard the object. Only a method taken off on its own,val f = o.m, is the bare function:
class Tally
var n
add(self, by) = self.n + by
val t = Tally.new(10)
print(t.add?.(1), t["add"]?.(2), t?.add(3), t?.["add"](4))
print(t.nope?.(1) ?? "none", null?.add(1) ?? "none")
val f = t.add
print(f(t, 5))
11 12 13 14
none none
15
- Nothing in the skipped part runs — not a key, not an argument, not a call:
var calls = 0
side() =
calls += 1
0
val gone = null
print(gone?.rows[side()].pick(side()) ?? "none")
print(calls)
none
0
o?.f = vandxs?.[i] = vare refused: there is no answer to what writing into absence should do.
val xs = null
xs?.[0] = 1
cannot be assigned to
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.
Spread
f(...xs) exists because a computed argument list had no spelling at all, and an array literal and
an object literal take one too — [...xs, y] is concat(xs, [y]) written the way a reader expects,
and { ...o, b: 2 } is o with { b: 2 }:
show(...parts) = join(parts, "-")
val xs = ["b", "c"]
val o = { a: 1 }
print(show(...xs))
print(show("a", ...xs, "d")) // in any order, any number of times
print([...xs, "d"], { ...o, b: 2 })
b-c
a-b-c-d
["b", "c", "d"] {a: 1, b: 2}
What may be spread is exactly what a for walks and what array(x) takes:
an array, bytes, a range, a generator, a set or a map. A map spreads its pairs and a buffer the
number at each position; a generator is run out, which is the only way there is to know what it
holds. A string is deliberately not on that list — chars(s) is how slate spells one as its
characters, and a spread that flattened text would be a surprise in every program that meant to pass
one string.
counting()
yield 1
yield 2
print([...0..<3], [...counting()], [...Set(["a"])])
[0, 1, 2] [1, 2] ["a"]
Spreading anything else is a fault naming the spread rather than the call, because that is where the reader has to look. 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.