Skip to content

Latest commit

 

History

History
193 lines (139 loc) · 5.07 KB

03-functions.md

File metadata and controls

193 lines (139 loc) · 5.07 KB

03: Functions

Declarations

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.

Returning Values

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

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.

Divergence and never

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`.
}

Function Expressions

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() {})();

Function Types

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); // 6

Function types are copy types, so binding one copies the reference to the function rather than allocating anything.

Closures

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); // 9

Capture Strategies

Polyscript 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.