Effects: pure fn and uses {…}

The compiler infers what every function does besides computing its result: whether it reads files, writes to a stream, changes a shared object, and so on. You never have to write effects down. When you do, the declaration is a promise the compiler checks.

pure fn area(int w, int h) -> int {
    return w * h;
}

fn save(string path, string text) uses {write, suspend} {
    // …
}

pure fn promises no effects at all. uses {…} lists the effects a function may have; uses {} means the same as pure. A function can have one or the other, not both. The clause comes after where, when there is one: fn name<A, B>(…) -> R where Trait<A, B> uses {…} { … }.

The effects#

NameCovers
readReads a file, a stream, the clock or a static let
writeWrites a file or a stream
netOpens or uses a socket
envReads or changes arguments, environment variables or the working directory
execRuns another program or ends this one
ffiCalls foreign code
threadSpawns, joins or locks across threads
suspendMay park until IO is ready (io::sync helpers do)
mutateWrites to an object the function did not create, or to a static let

A panic is not an effect. Bounds checks can panic almost anywhere, so a pure fn may index an array, assert, or panic. Building and changing an object the function made itself is not an effect either:

pure fn squares(int n) -> Vec<int> {
    let out: Vec<int> = Vec::new();
    for i in 0..n {
        out.push(i * i);
    }
    return out;
}

Calling a method that changes self on such an object is not an effect either, nor is passing the object to a function that changes only what it is given:

class Counter {
    pub n: int,
}

impl Counter {
    pub fn bump() {
        self.n = self.n + 1;
    }
}

pure fn counted(int k) -> int {
    let c = new Counter(k);
    c.bump();
    c.bump();
    return c.n;
}

This holds only while the object stays the function’s own. The call is mutate again when the object is stored in a field, a static let or a Vec, captured by a lambda, passed to a call together with an object the function did not make, passed to a call whose result could alias it, or returned from the function other than as a readonly view. A method that also writes anything else (a static let, an object the function did not make) is mutate too.

When a declaration does not hold#

The compiler reports E0413 with the call chain that needs the missing effect, and suggests the declaration that would hold:

Error: [E0413] `load` is declared `pure` but needs read, write: load → parse_file → open needs read, write
Help: declare `uses {read, write}`

Functions passed as arguments#

A declaration covers what the function does itself. A higher-order function that calls a function it was passed keeps that call open: each caller pays for the function it passes.

fn apply(int -> int f, int x) -> int uses {} {
    return f(x);
}

apply(fn (int x) => x + 1, 1) is pure; apply(bump, 1) has bump’s effects.

Trait methods#

A trait method can declare its effects. Every impl must keep to the declaration, and a call through the trait (an existential such as Area a) gets the declared effects. A call through a trait method with no declaration has unknown effects, so a pure fn cannot make one. The built-in traits (Show, Num and the operator traits) declare no effects yet, so that includes show(x) and arithmetic on a generic T.

trait Area<A> {
    pure fn area(A self) -> int;
}

pure fn total(Area a, Area b) -> int {
    return area(a) + area(b);
}

Seeing inferred effects#

Hovering a function in the editor (coil lsp) and coil dissect --effects show the inferred effects in the same vocabulary, with a reason for each:

step: uses {read, mutate}: writes static `HITS` (mutate); reads static `HITS` (read)
sq: pure
apply: pure apart from its parameters: calls parameter `f`

Auto-parallelization splits loops over pure functions only; see Threads.

↑ ↓ navigate ↵ open