Closures and function values
A closure is a function without a name that you write inside an expression. It can use the local variables of the code around it. A function value is a function that you keep in a variable or give to another function. This chapter shows how to write closures, how they hold the locals they use, and how to pass closures and named functions to other functions.
Each program in this chapter is also a test of the compiler. You can find it under
tests/backend/pass/ with the name that its section gives.
Writing a closure
A closure starts with its parameters between two | characters. Each parameter has a type. A
short closure has one expression as its body:
let double = |x : i64| x * 2;
A longer closure has a block as its body. Then you write the return type after ->, as for a
function. A closure without parameters starts with ||. You call a closure as you call a function:
double(4i64). Write the type of an integer argument, as in 4i64. A call of a closure does not
convert the literal 4 to an i64 yet (Vx#3234).
When a closure uses a local of the code around it, the closure captures the local. The compiler looks at what the body does with the local, and chooses one of three ways:
- The body only reads a number or a
bool. The closure keeps a copy of the value. - The body changes the local. The closure borrows the local as
&mut, so each call changes the local itself, not a copy. - The body only reads a struct. The closure borrows the struct as
&.
A borrow lasts until the last use of the closure, and the rules of
Ownership and borrowing apply to it. This program shows the two forms and the three
ways (book_closures_capture.vx):
struct Counter {
count : i64,
}
fn main() -> i32 {
// A short closure: the body is one expression.
let double = |x : i64| x * 2;
// A closure with a block body and a return type. It copies `k`.
let k : i64 = 10;
let add_k = |x : i64| -> i64 {
x + k
};
println!("double {} add_k {}", double(4i64), add_k(4i64));
// This closure changes `total`, so it changes the local itself.
let mut total : i64 = 0;
let add = |x : i64| -> void {
total += x;
};
add(5i64);
add(7i64);
println!("total {}", total);
// This closure only reads `c`, so it borrows `c`.
let c = Counter {
count : 3
};
let peek = || -> i64 {
c.count
};
println!("peek {} count {}", peek(), c.count);
return 0;
}
It prints double 8 add_k 14, then total 12, then peek 3 count 3. The closure add changed
total twice. main reads c.count after peek, because peek only borrowed c.
The compiler checks each way:
- A closure keeps the value it copied. If the program writes the local after it makes the closure and then calls the closure, the closure does not see the write. The compiler gives the warning W1034. A later release makes this an error.
- While a closure holds a local as
&mut, the code around it cannot read or write that local before the last use of the closure (E4002). - A closure whose body moves a struct, for example by passing it by value to a function, takes the struct. After that, the code around the closure cannot use the struct (E4001).
Inside a closure, ? needs the return type written:
|x : i32| -> Result<i32, E> { .. }. Vx for Rust programmers says
more about ?.
A closure that does no work while the program runs, but runs while compiling, has a comptime
body. Compile-time evaluation describes it.
Passing a closure to a function
A closure type names the parameters and the result of a closure: |i64| -> i64 takes an i64
and returns an i64. || -> i64 takes nothing. A function parameter with a closure type accepts a
closure, and the function calls it as f(x).
A closure that you write directly as an argument gets the parameter’s type. A closure that you keep
in a variable needs as and the closure type: call_twice(next as || -> i64). A generic function
can use its type parameters in the closure type, for example f : |T| -> T.
The standard library takes closures too. Option::map, Result::map and the iterator methods such
as fold and any take a Closure1<T, U> (one argument) or a Closure2<A, B, U> (two
arguments). These types are in core::ops. You can give such a method a closure literal or a
closure in a variable, with no as (book_closures_as_arguments.vx):
import core::option;
// `f` is a closure that takes an i64 and returns an i64.
fn twice(f : |i64| -> i64, x : i64) -> i64 {
return f(f(x));
}
// A generic function: `T` comes from the arguments.
fn apply<T>(x : T, f : |T| -> T) -> T {
return f(x);
}
fn call_twice(f : || -> i64) -> i64 {
let a = f();
return a + f();
}
fn main() -> i32 {
let step : i64 = 4;
println!("twice {}", twice(|x : i64| x + step, 5));
println!("apply {}", apply(7i64, |v : i64| v * 3));
// A closure in a variable needs `as` and the closure type.
let mut count : i64 = 0;
let next = || -> i64 {
count += 1;
count
};
println!("calls {} count {}", call_twice(next as || -> i64), count);
// `Option::map` takes a `Closure1<T, U>`.
let ten : i64 = 10;
let m = Option<i64>::Some(5).map(|x : i64| x + ten);
println!("map {}", m.unwrap_or(0));
return 0;
}
It prints twice 13, then apply 21, then calls 3 count 2, then map 15. The closure next
changed count on each of its two calls. The borrow of count ended with the call to
call_twice, so main can read count after it.
A function cannot return a closure that uses a local. The closure would point into the function’s stack frame, and the frame is gone after the return (E4005). For the same reason, a function cannot store such a closure through a reference that it gets. A closure that uses no local can be returned:
fn make() -> || -> i32 {
let c = || 40 + 2;
return c as || -> i32;
}
Calling a Closure1 in your own function
There is no call syntax for a Closure1 value yet. A Closure1 is a struct with two fields: env
points at the locals that the closure captured, and func is the code. To call it, you take func
and give it env as the first argument:
import core::ops;
fn apply_c1(f : Closure1<i64, i64>, x : i64) -> i64 {
let p = f.func;
return p(f.env, x);
}
The standard library calls its closures this way. In your own code, a closure type such as
|i64| -> i64 is shorter and safer, because env is a raw pointer.
Function pointers
A named function is a value too. Its type is a function pointer type, written with fn:
fn(i64) -> i64 takes an i64 and returns an i64. You give the function by its name, with no
parentheses. A variable or a struct field can hold it, and a function parameter can take it
(book_closures_function_pointers.vx):
fn square(x : i64) -> i64 {
return x * x;
}
fn double(x : i64) -> i64 {
return x * 2;
}
fn say_hello() -> void {
println!("hello");
}
fn seven() -> i64 {
return 7;
}
fn apply(f : fn(i64) -> i64, x : i64) -> i64 {
return f(x);
}
fn run<T>(f : fn() -> T) -> T {
return f();
}
struct Step {
op : fn(i64) -> i64,
}
fn main() -> i32 {
println!("apply {}", apply(square, 5));
let g : fn(i64) -> i64 = double;
println!("g {}", g(4i64));
let s = Step {
op : double
};
let op = s.op;
println!("step {}", op(3i64));
run(say_hello);
println!("run {}", run(seven));
return 0;
}
It prints apply 25, g 8, step 6, hello and run 7, each on its own line. run is generic,
and T is void for say_hello, a function that returns nothing.
To call a function pointer in a struct field, first copy the field into a variable, as op above
does. A call such as (s.op)(3i64) does not compile yet
(Vx#3234).
Function pointer types and closure types are different types. A function that takes
fn(i64) -> i64 does not accept a closure, and a function that takes |i64| -> i64 or a
Closure1 does not accept a named function (E3003). A function pointer holds no captured locals,
so a closure cannot fit in one.
unsafe fn values
An unsafe fn has the type unsafe fn(..) -> .., so its contract stays with the value. You can
call it through a variable only in an unsafe block (E5001). You cannot give it to a parameter of a safe
function pointer type:
unsafe fn raw_double(x : i64) -> i64 {
return x * 2;
}
fn apply(f : fn(i64) -> i64, x : i64) -> i64 {
return f(x);
}
fn main() -> i32 {
// Error E3003: expected fn(i64) -> i64, got unsafe fn(i64) -> i64
let y = apply(raw_double, 4);
return 0;
}
If a safe function accepted it, any safe code could call the unsafe fn through that function,
without an unsafe block. Unsafe and FFI explains unsafe fn.
Closures and tasks
Do not wait for a task inside a closure yet. A wait inside a closure blocks the thread that runs
it, so other tasks cannot use that thread (Vx#3212). A spawn in a closure is an error
(E3056). Tasks and channels shows where a spawn can go.