let binds immutably, var mutably. The primitive types are Int, Float, Bool
and String. A top-level function has to write out every parameter type and its return
type — the signature is the contract.
fn square(x: Int) -> Int = x * x
fn abs(x: Int) -> Int =
if x < 0 { 0 - x } else { x }
pub fn main() -> Unit !io = {
var total = 0
total = total + square(3)
total = total + abs(-4)
println(to_string(total))
}
13
Dot calls: x.f(a) is f(x, a)#
When a function is about its first argument, write that argument first. x.f(a) is exactly
f(x, a): the receiver fills the first parameter and the parentheses hold the rest. A chain of
dot calls reads in the direction the data flows, where nested calls read inside out:
fn double(x: Int) -> Int = x * 2
fn inc(x: Int) -> Int = x + 1
pub fn main() -> Unit !io = {
let n = 5
n.double().inc().to_string().println()
let total = [1, 2, 3, 4]
.filter(x => x > 1)
.map(double)
.fold(0, (acc, x) => acc + x)
println(total.to_string())
}
11
18
Two or more steps on the same value are always a chain, never nested calls: write
e.insert(5).insert(2), not insert(insert(e, 5), 2). A long chain goes one step per line,
with the . at the start of each continuation line. Calls with no natural subject stay
ordinary calls: constructors (Node(l, v, r)), range(0, n), println("hi"), and symmetric
functions like max(a, b).
A dot call only finds a function that is in scope under its bare name: your own functions,
prelude builtins, and names imported selectively. After a plain use std/str, s.split(",")
is an error, and the message says to write str.split(s, ",") or to add use std/str.{split}.
The spec has the full resolution order (§4.3).
The pipe |>#
The pipe is the same call in a different spelling: x |> f(a) is f(x, a). Reach for it when
the dot does not fit: the data starts as a literal or a bigger expression, the last step is a
sink or a lambda, the function is module-qualified and you would rather not import its bare name
(s |> str.split(",")), or the right-hand side is a function value rather than a call. It binds
looser than everything else, so a + b |> f is f(a + b). A chain uses either dots or pipes,
not both.
fn double(x: Int) -> Int = x * 2
fn inc(x: Int) -> Int = x + 1
pub fn main() -> Unit !io =
5 |> double |> inc |> to_string |> println
11
Named arguments and default values#
A parameter may carry a default, written name: Type = expr, and a call may then leave
that argument out. Any argument may also be passed by name: the positional ones fill the
slots from the left, a named one takes the slot it names, and names may come in any
order.
fn greet(name: String, greeting: String = "Hello", punct: String = "!") -> String =
"${greeting}, ${name}${punct}"
pub fn main() -> Unit !io = {
println(greet("Dawn"))
println(greet("Dawn", punct: "?"))
println(greet(punct: ".", name: "reader", greeting: "Welcome"))
}
Hello, Dawn!
Hello, Dawn?
Welcome, reader.
A default is evaluated afresh on every call that leaves it out, and it has to be pure:
if it could do io, whether a call did io would depend on whether the caller passed that
argument. Arguments run in the order they are written, whichever slots they land in.
The names belong to the signature, so a function value has none: after let g = greet,
g(name: "x") is an error and g wants all three arguments. Constructors take names the
same way, their field names standing in for parameter names (Rect(w: 3.0, h: 4.0)).
See also: spec.en.md §2.1, §3.1, §4.3, §4.4