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#
| Name | Covers |
|---|---|
read | Reads a file, a stream, the clock or a static let |
write | Writes a file or a stream |
net | Opens or uses a socket |
env | Reads or changes arguments, environment variables or the working directory |
exec | Runs another program or ends this one |
ffi | Calls foreign code |
thread | Spawns, joins or locks across threads |
suspend | May park until IO is ready (io::sync helpers do) |
mutate | Writes 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.