slate

Functions

There is no keyword on a function. The shape is what identifies it: a name, a parameter list in brackets, and then either = and an expression or an indented block.

double(x) = x * 2

add(a, b)
    a + b

grade(mark)
    if mark >= 90
        "A"
    else
        "C"
end grade

print(double(21), add(1, 2), grade(95), grade(20))
42 3 A C

A block’s value is its trailing expression, so return is for leaving early and nothing else.

Functions are values. A definition binds a name in the scope it is written in, so a nested definition is a closure over that scope:

counter()
    var count = 0

    bump()
        count = count + 1
        count

    bump

val c = counter()

print(c(), c(), c())
1 2 3

Lambdas

-> with the parameters on its left:

val double = x -> x * 2
val add = (a, b) -> a + b
val zero = () -> 0

print(double(21), add(1, 2), zero())
42 3 0

-> is the one right-associative operator, so x -> y -> x + y is a function answering a function.

A lambda’s body may be a block, written where the lambda is passed. A newline inside brackets normally means nothing, so a callback would otherwise have to be lifted out and named before the call that wanted it:

forEach([1, 2, 3], x ->
    val doubled = x * 2
    print(x, doubled))
1 2
2 4
3 6

-> and match are the two tokens that suspend the bracket rule, and only where they end a line. A , that ends its line, at the block’s own bracket depth, ends the block — so a block lambda need not be the last argument: setTimeout(() -> … , and then 1000) on the next line keeps node’s order and still takes a block body. A comma written mid-line is the statement’s own, which is what keeps a, b = b, a whole inside such a body, and one deeper in brackets closes nothing.

A lambda’s parameters may be annotated; its result may not, the arrow already standing between the parameters and the body. What it answers is read off that body — see Types.

_, a lambda with its parameter left out

A _ where a value goes is the parameter of a function nobody wrote. What that function’s body is is the smallest thing around the _: a call’s argument, a bracketed group, or the value of a binding, an assignment or a return.

val ns = [1, 2, 5, 9]

print(map(ns, _ * 2))
print(filter(ns, _ > 3))
print(map(["ada", "grace"], upper(_)))
[2, 4, 10, 18]
[5, 9]
["ADA", "GRACE"]

Every _ is a parameter of its own, left to right. Two of them make a function of two parameters, which is exactly what a comparator wants:

val people = [{ name: "grace", age: 45 }, { name: "ada", age: 36 }]

print(map(sorted(people, _.age < _.age), _.name))
["ada", "grace"]

That rule is also this notation’s one surprise. _ > 3 && _ < 9 is a function of two parameters and not one test of one number, so it is refused where a callback taking one was wanted. Write the parameter out when you mean to mention it twice: n -> n > 3 && n < 9.

A lone _ is handed outward to the thing around it, which is what makes f(_) a way of naming f rather than a way of handing it an identity — and what makes add(_, 1) the partial application it reads as. Where there is nothing around it, a _ on its own is the identity function:

add(a, b) = a + b

print(map([1, 2, 3], add(_, 1)))

val id = _

print(id(7))
[2, 3, 4]
7

A _ with no argument, no group and no right-hand side around it has nothing for its function to be, and is refused where it stands:

_.length
`_` stands for the parameter of a function

_ in a pattern is untouched. A match arm’s _, a destructuring’s and the name in val _ = f() all mean the wildcard they always did — the notation here is about _ standing where a value goes.

_ in a PARAMETER position is a discarded parameter, not a placeholder — _ -> 7 and (_, x) -> x are ordinary lambdas whose _ parameter cannot be read back, the same idiom JavaScript writes as _ => 7; the placeholder rule above is only for a _ where a value goes.

Type parameters

[T] after the name says the definition is generic over a type, and the answer is said in terms of what the call was given:

first[T](xs: array of T) -> T = xs[0]

print(first(["ada", "grace"]), first([1, 2]))
ada 1

There are no type arguments at a call — they are solved from the arguments, and every argument has to fit the type the parameter was solved to, so pair(1, "x") for pair[T](a: T, b: T) is refused. Types says how the answer is picked and what a union does.

Defaults

A parameter may carry what it is when nobody gives one, on a definition, a lambda, a method, or a class’s new. The annotation comes first and the default after it:

greet(name, greeting = "hello") = greeting + ", " + name
f(n: integer = 0) = n

print(greet("ada"))
print(greet("ada", "hi"))
print(f(), f(7))
hello, ada
hi, ada
0 7

The default is worked out at the call, not where the function was written. Everything else follows from that:

  • f(xs = []) gives every call an array of its own.
  • A default may read the parameters to its left: slice(xs, from, to = xs.length).
  • A default that would fault costs nothing to a call that gave the argument.

A default may not read its own parameter or one to its right, since those are not bound yet when it runs. That holds anywhere in the default — a lambda written inside one may not mention a later parameter either, since nothing says when the lambda runs — and it is refused before the program runs:

t(v) = v
early(a = t(b), b = 2) = a
print(early(b = 3))
the default of `a` reads `b`, which is to its right -- a default sees only the parameters before it

A pattern’s defaults follow the same rule: in val { a = 1, b = a + 1 } = o, b may read a and a may not read b.

A parameter that may be left out has to come last, or leaving it out would slide every later argument one place left. The parser says so where it is written, and an arity complaint then names a range rather than only its upper end.

The guard is whether the call gave the argument, not what it gave, so f(1, null) passes null and does not take the default — the simpler rule, where JavaScript’s f(1, undefined) does the opposite. There is nothing to write in its place either way: an absence cannot be passed to a function, so f(1, undefined) is refused where it is written.

Optional parameters

b? marks a parameter optional with no value of its own. It comes out of the count exactly as a default does — a call may leave it off — and where nobody gives it, it reads as the absence a missing field reads as, which ?? resolves:

greet(name, title?) = (title ?? "friend") + " " + name

print(greet("ada"))
print(greet("ada", "captain"))
friend ada
captain ada

It is b = undefined written in one character, and it obeys the same rule defaults do: a parameter that may be left out has to come last. Reach for a default where there is a sensible value and for ? where the whole point is that there is none.

Named arguments

An argument may say which parameter it fills, which is what makes a default in the middle reachable:

greet(name, greeting = "hello", punct = "!") = greeting + ", " + name + punct

print(greet("ada"))
print(greet("ada", punct = "?"))            // greeting skipped
print(greet(greeting = "hi", name = "ada"))
hello, ada!
hello, ada?
hi, ada!

= and not :, because the declaration already writes the default after an equals. Assignment is a statement in slate, so = never appears inside an expression and there is nothing for it to be confused with; == is its own token, so f(ok == true) is an ordinary positional argument.

A name comes after every positional argument, and it may pick out any parameter. A class’s new and a data variant’s maker are ordinary functions, so Rect(h = 4, w = 3) and Circle(r = 7) read the same way. A method names its parameters and not its receiver.

Naming a parameter twice, naming one the function does not have, naming an argument to a builtin, or naming one to a function that gathers with ... are each refused with their own sentence — the second lists the parameters it does have.

...rest

A function may gather what is left over:

total(first, ...others) = reduce(others, (a, b) -> a + b, first)
val xs = [1, 2, 3]

print(total(1))
print(total(1, 2, 3))
print(total(...xs))         // the spread it is the counterpart of
1
6
6

...rest is always bound, to an empty array where a call gave nothing past the fixed parameters, so there is no absence to test for. It must be last and takes no default — one could never fire. A default before it is fine.

The call rule, which is JavaScript’s at run time and TypeScript’s where the callee can be seen

At run time a call binds what the function declares and nothing else. A surplus argument is dropped, and a parameter nobody gave reads as an absence — exactly JavaScript’s rule, on both back ends. This is what a call through a value does, which is every call something else makes on your behalf:

call3(g) = g(1, 2, 3)
f(a) = a

print(call3(f))
print(call3((a, b) -> a + b))
print(map([1, 2, 3], v -> v * 2))
print(map([1, 2, 3], () -> 9))
1
3
[2, 4, 6]
[9, 9, 9]

This is JavaScript’s rule and TypeScript’s, and it is what lets a function be used wherever a wider one is wanted. It is what a handler wants to look like — on(node, "click", () -> setCount(n + 1)) for one that does not read the event, onData(socket, () -> stop()) for a reader that does not care what arrived — and it holds everywhere something calls a function you wrote: array walks, sorted, timers, sockets, WebSocket handlers, the document‘s events, a method reached off an object, a function handed out to a JavaScript host.

setTimeout(() -> print("later"), 0)

on2(f) = f(1, 2)

print(on2(a -> a))
1
later

Where the call is WRITTEN OUT and the checker can see what it is calling, a wrong count is refused before the program runs. That is TypeScript’s half of the same rule, and it is the half that catches mistakes: a call you wrote by name is a call you meant, so a count that does not match is worth saying rather than quietly obeying. Too few:

f(a, b) = a + b

print(f(1))
`f` takes 2 arguments and this gives it 1

Give the parameter a default, or mark it optional with ?, where leaving it out is meant to be allowed. And too many, at the same call:

f(a) = a

print(f(1, 2))
`f` takes 1 argument and this gives it 2

A builtin is counted the same way, its signature being fixed and the call therefore always one the checker can see:

print(chars("a", "b"))
`chars` takes 1 argument and this gives it 2

A function handed over as a VALUE is judged by whether it FITS, never by a count, which is why the first block on this page runs: a callback declaring fewer parameters than its caller supplies is what x -> x * 2 is. One declaring MORE than its caller will supply is refused, and the complaint names the caller, your function not being the thing that is wrong:

map([1], (a, b, c, d) -> a)
`map` takes (integer, integer, array of integer) -> any here, and this is (integer, integer, array of integer, any) -> integer

That is the checker, which knows what map hands over — the element, its position and the array, so three is what there is and a fourth parameter has nothing to fill it. Reached through a value it cannot see, nothing refuses it and the fourth parameter is simply absent.

The same rule read as a type: a function of fewer parameters fits wherever more are supplied, and one of more does not.

apply(f: (integer) -> integer) = f(1)

print(apply((a, b) -> a))
`apply` takes (integer) -> integer here, and this is (any, any) -> any

A parameter the call left out reads as an absence

Where a call the checker could not see gives too few, the parameters nobody filled read as undefined — the same value a missing field reads as, resolved the same ways:

call1(g) = g(1)

print(call1((a, b) -> b == null))
print(call1((a, b) -> b ?? "gone"))
print(call1((a, b) -> if b then "yes" else "no"))
true
gone
no

And it goes no further than the read. An absence cannot be stored in a container, bound to a name, passed on to another function or returned, so a function that puts an unfilled parameter into an array or hands it back faults there rather than carrying the hole along — which is the rule that keeps undefined from surfacing far from where it came.

Destructuring parameters

A parameter may take its argument apart, on a definition or a lambda:

f({ n }) = n * 2
val g = ({ n }) -> n + 1

print(f({ n: 21 }), g({ n: 1 }))
42 2

The pattern is any pattern that binds.

Annotations

Per parameter, and per result:

type Point = { x: number, y: number }

double(x: number) -> number = x * 2
f(a, b: Point, c) = b.x + a + c     // nothing has to be annotated for anything to be

print(double(3), f(1, { x: 2, y: 0 }, 3))
6 6

An annotation is checked at the call for a parameter, and at every way out for a result — the value a return carries as much as the one the body falls out with. See Types for what the compiler will say about one before the program runs.

Contracts

An annotation says what a value is. A contract says what has to be true of it. require is checked on the way in and ensure on the way out, where result names what the function answered:

withdraw(balance, amount)
    require amount > 0
    require amount <= balance
    ensure result >= 0
    balance - amount

print(withdraw(100, 30))
print(withdraw(100, 200) catch e -> e.message)
70
`withdraw` requires `amount <= balance`, and this call does not meet it

Both are clauses at the top of the body, above every other statement, and both are checked when the program runs. A clause written further down is refused where it stands:

f(n)
    val m = n * 2
    require m > 0
    m
a `require` clause is a function's contract, and belongs at the top of its body

A failed clause is an ordinary fault, naming the function and quoting the clause. A postcondition names the answer as well, which is the thing the clause is about and the thing that is not written above it:

clamp(n)
    ensure result >= 0
    if n < 0
        return n
    n

print(clamp(5))
print(clamp(-5) catch e -> e.message)
5
`clamp` ensures `result >= 0`, and gave back -5

ensure covers every way out and not only the last line — clamp above answers through a return, and the clause is checked there too.

A condition is an ordinary expression read for its truth, so anything a program can compute may stand in one; nothing about a clause is worked out before the program runs. What the compiler will say about one is what it says about the expression: a name that is not bound, a call of the wrong arity, a value that cannot be what a function takes. It never tries to decide whether a clause holds.

old

A postcondition about a function that changes something needs the value as it was on the way in. old(e) is e evaluated on entry, kept for the clause to read on the way out:

bump(counter)
    ensure counter.n == old(counter.n) + 1
    counter.n += 1
    counter.n

val c = { n: 0 }

print(bump(c))
print(bump(c))
1
2

It takes exactly one expression, and it is a snapshot only inside an ensure — a require is checked on entry, where old(e) and e are the same value, so writing one there is refused:

withdraw(balance, amount)
    require amount <= old(balance)
    balance - amount
`old(e)` is what `e` was on entry, and belongs in an `ensure`

Every old in a function is its own value, so two in one clause say two different things and each clause may have its own. Each call takes its own snapshots, which is what makes a contract on a recursive function mean what it looks like it means.

What is kept is a VALUE, not a place. old(items) is the array the function was handed — the same array the body then changes — so a length read off it afterwards is the length it has now. old(items.length) is what asks for the length it had on entry:

grow(items)
    ensure old(items).length == items.length
    push(items, 9)
    items

print(grow([1, 2]))
[1, 2, 9]

old is a soft word like the two clauses themselves: outside an ensure it is an ordinary name, so a program that already has one keeps working.

old(v) = v + " (as it was)"

print(old("the name"))
the name (as it was)

require and ensure are soft words. A program that already uses either as a name is untouched, because they are clauses only at the head of a body:

val require = (path) -> "loaded " + path

print(require("./config.sl"))
loaded ./config.sl

async and generators

async in front of a definition or a lambda makes it answer a promise; a function holding a yield is a generator, with no word on the definition. Both are in Asynchrony.

Methods

A function stored in an object field is a method. Whether it is handed a receiver depends on where it was found — see Objects.

Search

Esc
↑↓ to navigate ↵ to open Esc to close