A function is declared with fn, a name, a parenthesized parameter list, an optional return type, and
a body. Every parameter carries a type annotation.
fn add(x: int, y: int): int {
return x + y;
}
// A function with no annotated return type returns `void`.
fn greet(name: str) {
print(name);
}The return type is written after a colon, in the same position a variable's annotation occupies.
NOTE: Programs have no entry point, so functions are declared at the top level of a file alongside ordinary statements, and are called from the top level as well.
A function body is a block, and a block resolves to the value of its final expression. That value is the function's return value.
fn double(x: int): int {
x * 2 // No semicolon: the block resolves to this, and the function returns it.
}The return keyword returns immediately from anywhere in the body, and is required when returning
from a point other than the end.
fn clamp(x: int, hi: int): int {
if (x > hi) {
return hi;
};
x
}A bare return with no operand returns from a void function.
fn maybe_print(flag: bool, message: str) {
if (!flag) {
return;
};
print(message);
}Parameters are values local to the function. Assigning to one rebinds it inside the body only; the
argument the caller passed is unaffected. A parameter that the body reassigns may be marked mut to
say so.
fn countdown(mut n: int) {
while (n > 0) {
n -= 1;
}
}
let start = 3;
countdown(start); // `start` is still 3.Arity is checked at every call. Passing the wrong number of arguments is an error.
The never type introduced in Syntax and Basics is the type of an
expression that never produces a value. A function that never returns is annotated with it.
import { exit } from "stdlib/process.ps";
fn die(message: str): never {
eprint(message);
exit(1); // `exit` is itself `never`, so this function diverges.
}A never function must actually diverge on every path, and a function that diverges on every path
must be annotated never. The two rules are enforced together, so the annotation always tells the
truth.
Because never unifies with every other type, a call to a diverging function is accepted wherever a
value of any type is expected. This is what allows a bail-out to sit in a branch that must otherwise
produce a value.
enum Player { X = -1, O = 1 }
fn opponent(p: Player): Player {
if (p as int == -1) { Player.O }
else if (p as int == 1) { Player.X }
else { die("not a player") } // `never` does not conflict with `Player`.
}fn is also an expression. Written without a name, it produces an anonymous function that can be
bound to a variable, passed along, or invoked on the spot.
let combine = fn(x: int, y: int): int {
x + y
};
let sum = combine(3, 4);An anonymous function may be called immediately, at the point it is written.
(fn() {})();The type of a function is written with fn, the parameter types in parentheses, and an optional
return type after a colon. It names a signature, not a particular function.
| Type | Describes |
|---|---|
fn(int, int): int |
Takes two ints, returns an int. |
fn(): int |
Takes nothing, returns an int. |
fn(str) |
Takes a str, returns void. |
fn() |
Takes nothing, returns void. |
A named function may be referred to by name and stored in a variable of the matching type.
let f: fn(int): int = double; // `double` from earlier in this chapter.
let g: fn(): int = fn(): int { 7 };
let value = f(3); // 6Function types are copy types, so binding one copies the reference to the function rather than allocating anything.
A function that mentions a variable from an enclosing scope captures it, becoming a closure. There is
no separate syntax — any fn, named or anonymous, becomes a closure by referring to an outer binding.
let n = 5;
fn add_n(y: int): int {
n + y // `n` comes from the enclosing scope.
}
let result = add_n(4); // 9Polyscript inspects how each captured variable is used and captures it accordingly. The choice is automatic and has no syntax.
| Strategy | Chosen when | Effect |
|---|---|---|
| Copy | The variable is only read, or passed onward by value. | The value is copied into the closure when the closure is created. |
| Mutable reference | The variable is assigned inside the body. | The closure refers to the original binding, and its writes are visible outside. |
A variable captured by copy is a snapshot. A variable captured by mutable reference is shared.
let total = 0;
fn accumulate(x: int) {
total += x; // Assigned, so captured by mutable reference.
}
accumulate(5);
accumulate(3);
// `total` is 8 here.Captures are resolved per variable, so one closure may copy one binding and mutably borrow another.