slate

Values

slate is dynamically typed. Every value is one of a fixed set of kinds, and a program can ask which.

The kinds

kindwrittennotes
nullnullthe only absence there is
booleantrue, false
integer42, 0xffsigned, no width — it grows rather than wrapping
real3.14, 2e10a double
string"text"a sequence of characters, never of bytes
array[1, 2]reference type, compares by contents
bytesbytes([1, 2]), toBytes(s)reference type, compares by contents — Bytes
object{ a: 1 }reference type, compares by contents
functionx -> x, a definition
promiseanswered by an async callsee Asynchrony
setSet([1, 2])reference type, holds each value once — Collections
mapMap([["a", 1]])reference type, any value as a key — Collections

A set and a map compare by identity rather than by contents, as a promise and a function do: what is in either can change, so an answer read off the contents would stop being true without either value being touched by the code that asked.

Eight more scalar kinds come with slate:time — instant, duration, date, time, dateTime, zone, zoned, period — and a compiled pattern comes with slate:regex. Each has a type word that tests for it in pattern position.

number is not a kind. It is the union of integer and real, and exists because the two are separate values: a guard about arithmetic would otherwise have to be written twice.

Truth

slate follows JavaScript’s rule. false, null, an absent value, 0 (and -0), NaN and "" are false; everything else is true — including [], {}, "0" and an empty bytes buffer, which is an object in JS terms and stays true however long it is.

print(if 0 then "true" else "false")
print(if "" then "true" else "false")
print(if [] then "true" else "false")
false
false
true

?? is unchanged and asks a different question. It tests whether a value is absent, not whether it is true — zero and the empty string are values, not absences, so 0 ?? 5 is still 0 and "" ?? "d" is still "". See Logic.

Equality

== compares by value, all the way down. Two arrays holding the same elements are equal; two objects holding the same fields are equal.

print([1, 2] == [1, 2], { a: 1 } == { a: 1 })
true true

An integer and a real compare across the kinds, so 1 == 1.0 is true — but 1 is integer and 1.0 is integer still differ, the two being distinct values that happen to compare equal.

A function is equal to itself. f == f is true, and two separately written lambdas with the same body are not equal.

There is one special case, and it is the two absences: null == undefined is true. So x == null and x == undefined are the same question — is this absent, either way — and != is its negation. A field holding null and a field that is not there are still two different things, and has is what asks which one you have.

val o = { a: 1, b: null }

print(o.b == null, o.nope == null, o.b == undefined)
print(null == 0, null == false, undefined == "")
print(has(o, "b"), has(o, "nope"))
true true true
false false false
true false

Nothing else of JavaScript’s loose equality comes with it, which is why there is no === to escape back to. null == 0, null == false and undefined == "" are all false, and == is strict about every other pair of kinds. The == null idiom is taken deliberately because a read that found nothing is almost always to be handled the same way whichever kind of nothing it was; where it is not, has draws the line.

A class may take == over for its own instances by writing a method called ==, and must write hash beside it if those instances are to be used as table keys. != is always the opposite of == and may not be written. <, <=, > and >= are each a method of their own, or all four at once as <=> — see Objects.

Numbers

An integer never wraps: it grows. There is no largest integer and no smallest one, and no arithmetic on two integers ever answers a number other than the one it means. Python’s rule, and the 64-bit machine word is a fast path with nothing about it a program can see.

print(9223372036854775807 + 1)
print(1 << 100)
print(pow(2, 100))
9223372036854775808
1267650600228229401496703205376
1267650600228229401496703205376

A factorial is the number rather than a remainder of it:

factorial(n)
    var f = 1

    for i in 1..n
        f = f * i

    f

print(factorial(30))
265252859812191058636308480000000

A wide integer is an integer and nothing else. is integer is true of one, == compares the numbers, string gives the digits and a literal is read at whatever width it was written at — in any base, with separators if you like.

print(123456789012345678901234567890 is integer)
print(0xffffffffffffffffffff)
print(1_000_000_000_000_000_000_000_000)
print((1 << 100) - (1 << 100) + 5)
true
1208925819614629174706175
1000000000000000000000000
5

That last line is worth reading twice: a number that grew and came back is the same value as one that never left, so it compares equal to 5, hashes as 5 does, and finds an entry a table holds under 5. Nothing in the language distinguishes the two.

The bit operations read a value as an endless run of bits — a non-negative number padded upward with zeros and a negative one with ones — which is the only reading that needs no width to complement within. So ~x is -x - 1, << grows rather than discarding, and >> floors toward negative infinity: past the top of a number it answers -1 for a negative and 0 for anything else.

print(~5, -6 & 3, -6 | 3, -6 ^ 3)
print(1 << 100 >> 99, -1 >> 200, 7 >> 200)
-6 2 -5 -7
2 -1 0

A shift is bounded at 16,777,216 places and pow at an answer of that many bits. Neither is a width; they are the point at which slate says so rather than letting the allocator give up.

Where a call genuinely needs a machine integer — an array index, a repeat count, a byte — a number too wide for one is refused, and the refusal says so.

print([1, 2, 3][pow(10, 30)])
an array is indexed by an integer, and this is an integer too large to fit 64 bits

An integer and a real are compared exactly and their arithmetic promotes, which is Python’s rule and is two answers to two different questions. big + 0.5 has no answer in the integers, so it is a real; big < 0.5 has an exact answer always, and rounding the integer to a double first would make numbers a million apart compare equal. 1e30 is not the number 10 ^ 30 — it is the nearest double to it — and this says so:

print(pow(10, 30) == 1e30)
print(pow(10, 30) < 1e30)
print(integer(1e30))
print(pow(10, 30) + 0.5 is real)
false
true
1000000000000000019884624838656
true

A wide integer goes through JSON as its digits, JSON’s own grammar bounding a number at nothing. Reading them back grows the number again rather than rounding it to the nearest double, so a document carries what it was given; and an exponent is still a real however large it is, which is what keeps the two kinds apart across a round trip.

val doc = { n: pow(10, 30), scale: 1e30 }
val back = parseJSON(toJSON(doc)).value

print(toJSON(doc))
print(back.n == pow(10, 30))
print(back.scale is real)
{"n":1000000000000000000000000000000,"scale":1e+30}
true
true

/ ANSWERS A REAL, ALWAYS — even between two integers, and even where the division comes out exact. 7 / 2 is three and a half, and 4 / 2 is a whole real rather than the integer 2. The kind of the answer does not depend on the kinds of the operands, which is the whole of the rule: nothing has to work out whether a division came out evenly before it knows what it is holding. It is Python 3’s rule and JavaScript’s, and a wide integer reaches it the same way, through toReal — which may be an infinity for a number past what a double can name.

\ IS THE WHOLE-NUMBER DIVISION, and it takes two integers and answers an integer. It is spelled with a backslash because // already opens a comment in slate; the spelling is Visual Basic’s. Inside a string literal a backslash is still the escape it always was, the lexer reading a string on its own.

\ truncates toward zero and % takes the sign of the left operand, and those two facts are one fact: a == (a \ b) * b + a % b holds for every pair, and it holds only for the truncating division. A flooring \ would need a flooring % beside it, which is Python’s pairing and not slate’s.

print(7 / 2, 7 \ 2, -7 \ 2, -7 % 2)
print(4 / 2, 4 / 2 is integer)
print(-7 == (-7 \ 2) * 2 + (-7 % 2))
3.5 3 -3 -1
2 false
true

By zero the two behave differently, and that follows from the first rule rather than being an exception to it. / is a real division wherever it appears, so 1 / 0 is the infinity 1.0 / 0.0 has always been; \ and % are whole-number operators and fault, on both back ends.

print(1 / 0, -1 / 0, 0 / 0)
print(1 \ 0 catch e -> e.message)
Infinity -Infinity NaN
this divides by zero

\ is of integers and refuses a real, where / takes anything numeric. \= is its compound form, beside /=, and it binds at the * / % power.

A real that is whole prints as an integer does. string(1.0) is "1". Only %, indexing, or a kind test can tell the two apart, so a function that must answer an integer is worth annotating.

Otherwise a real prints the shortest text that reads back as the same double, exactly as JavaScript’s own String does — so 0.1 + 0.2 prints 0.30000000000000004 rather than rounding to a tidy 0.3 a program is not actually holding, and number(string(x)) is always x again.

Strings

A slate string is a sequence of characters, so every position, length and slice is counted the way a person counts:

print("日本語".length)
print("日本語"[0..<1])
print("héllo"[1])
print(indexOf("héllo", "llo"))      // by character; it is 3 by byte
3
日
é
2

There is no character type: a single character is a string of one. That is what lets indexing, chars and split all hand back the same kind of thing.

s.at(i) reads one character and counts back from the end where the position is negative, which is what xs.at(-1) is for an array and is the whole reason JavaScript grew at beside s[i]. It counts characters like everything else here, and a position past either end is a fault rather than an absence, slate storing no undefined for such a read to hand back.

A for walks a string’s characters, which is chars(s) without the array in the middle:

print("a👋b".at(-1), "a👋b".at(1))

for c in "a👋"
    print(c)
b 👋
a
👋

.length is that count read as a property — a name a . answers with a value and no brackets after it, exactly as a class’s get is read. s.length counts characters and not UTF-16 units, so a string of one emoji is 1 here where JavaScript says 2. An array carries one too, and so does a buffer, which is the shape the bytes under a string come back in.

print("日本語".length, "a👋".length)
print([1, 2, 3].length, toBytes("héllo").length)
3 2
3 6

It is read-only. A length is what a value already is rather than a field it holds, so writing to one is refused instead of resizing anything:

var s = "abc"

s.length = 5
`length` is a read-only property of a string

toBytes(s) answers a buffer and fromBytes(bs) answers a result; those two are the only place a slate program sees UTF-8, and toBytes(s).length is the byte count.

Case and whitespace are the whole database and not the ASCII range. upper and lower answer what any other language with a case table answers, which is not always one character out for one character in, and trim takes off Unicode’s White_Space — so a no-break space pasted out of a form comes off and a zero-width no-break space, which is not a space at all, stays.

print(upper("Straße"), lower("İ").length)
print(lower("ΟΔΟΣ"), lower("ΟΔΟΣΑ"))
print("[" + trim(" \u{a0}x\u{a0} ") + "]")
STRASSE 2
οδος οδοσα
[x]

The second line is Unicode’s own rule that a sigma ending a word is written ς, which is context rather than a table; the first is ß uppercasing to two letters and İ lowercasing to two.

normalize(s, form) puts text into one of "NFC", "NFD", "NFKC" and "NFKD", which is what two strings have to go through before == between them means what a reader thinks it means — the same word typed on two machines is routinely two different sequences of characters. casefold(s) is what two strings differing only in case both come to, and is not lower: ß folds to ss, so casefold("STRASSE") == casefold("Straße") where lowering leaves them different.

Conversion

The type words are the conversions. string, number, integer, real and boolean each test in pattern position and convert in expression position; the two never overlap.

print(string(123) + "!")
print(number("42"), number("nonsense"))
print(integer(2.9), real(3), real(3) is real)
123!
42 null
2 3 true

number answers null where the text is not a number, which is how a program checks input without raising. integer and real move between the two numeric kinds; the four roundings — floor, ceil, round, trunc — leave an integer alone.

min and max take as many arguments as they are given and answer an integer when every one of them was.

bytes is the one word that is a kind and a constructor without being a conversion of a value. bytes(n) makes a buffer of n zero bytes rather than reading n as one; the conversions between text and bytes are toBytes and fromBytes, since each needs an encoding named or defaulted. See Bytes.

Reference and copy

Arrays, buffers of bytes and objects are reference types: two names may hold the same one, and a write through one is visible through the other. Everything else is a scalar.

a with { f: v } answers a copy of a with f changed, which is how a record is updated without mutating it. concat(xs, ys) is the array counterpart.

Absence

null is the only absence a program can keep, it is an ordinary value, and slate refuses to store anything else in its place — undefined exists only as the immediate answer to a read that found nothing, or to a yield a bare next() resumed, and compares equal to null. That single rule explains a run of behaviour that otherwise looks unrelated:

  • pop, shift and at fault where there is nothing there, rather than answering nothing.
  • find and indexOf answer null — a search that found nothing is an answer, where reaching past the end is a mistake.
  • A parameter nobody gave reads as the absence a missing field does, so f(1, null) is not the same as f(1) — and like a missing field it can be read but not kept.
  • An object field a value need not have is a question about its shape (pinned?), not a value that might be absent.

Keeping one is refused where it is attempted — binding it to a val, a var or a for head, writing it over a local, storing it in a field, an array or an object, passing it to a function, or returning it from one — and in the same words under slate js:

val settings = { port: 8080 }
val host = settings.host

print(host)
this field is not there, and `undefined` cannot be bound to `host` -- give it a value with `??`, or ask `has` first

settings.host ?? "localhost" is the program that runs.

A function answering one is refused at its return, or where its body falls out, rather than at whatever the caller did with the answer — which is the line that read nothing:

hostOf(settings) = settings.host

val host = hostOf({ port: 8080 })
this field is not there, and `undefined` cannot be returned -- give it a value with `??`, or ask `has` first

A function with nothing to answer answers null, so hostOf(settings) = settings.host ?? null is the lookup that says maybe, and undefined written out as a function’s answer is refused before the program runs.

Search

Esc
↑↓ to navigate ↵ to open Esc to close