4 ms·
In many ways, closures can be more difficult when there is not garbage collection because we precisely do not want to refer to values that have been freed or co
by kxyvr 7y ago
In many ways, closures can be more difficult when there is not garbage collection because we precisely do not want to refer to values that have been freed or collected. For example, in C++, we can write code like:
#include <functional>
#include <iostream>
auto foo(double a) -> std::function <double(double)> {
return [&a](auto x) {
return x + a;
};
}
int main() {
auto f = foo(2.0);
std::cout << f(1.0) << std::endl;
std::cout << f(1.0) << std::endl;
}
This code is bad because the function returned by `foo` depends on a reference to `a`, which is freed at the end of `foo`. Running this gives:
3
1
which is bad. Rust has the advantage of finding such errors. For example, the following Rust code:
fn foo(a: f64) -> impl Fn(f64) -> f64 {
|x: f64| -> f64 { x + a }
}
fn main() {
let f = foo(2.0);
println!("{}", f(1.0));
println!("{}", f(1.0));
}
fails to compile with the error:
error[E0597]: `a` does not live long enough
--> test01.rs:2:27
|
1 | fn foo(a: f64) -> impl Fn(f64) -> f64 {
| ------------------- opaque type requires that `a` is borrowed for `'static`
2 | |x: f64| -> f64 { x + a }
| --------------- ^ borrowed value does not live long enough
| |
| value captured here
3 | }
| - `a` dropped here while still borrowed
That forces us to correct the code by adding the move annotation to the the closure, which moves ownership of a to the closure itself:
fn foo(a: f64) -> impl Fn(f64) -> f64 {
move |x: f64| -> f64 { x + a }
}
fn main() {
let f = foo(2.0);
println!("{}", f(1.0));
println!("{}", f(1.0));
}
This gives the correct result:
3
3
Of course, all of this nuance can be avoided in a garbage collected language. For example, in OCaml, we can write:
let foo a =
fun x -> x +. a
in
let f = foo 2.0 in
Printf.printf "%f\n" (f 1.0);
Printf.printf "%f\n" (f 1.0);
which also returns the correct result:
3.000000
3.000000
More generally, closures are nice because it's a useful feature to return a function from a function. For example, we may want call a function parameterized on some parameters repeatedly and we don't want to have to pass all of the parameters each time we invoke the function. Functional programming and closures allow for a variety of other programming techniques as well.
Beyond that, my point is that a garbage collector can track the use of variables and memory extremely well, which makes the process of constructing closures moderately easy. We can have closures without garbage collection, but then we must be careful about who and what owns this memory. C++ does not enforce this, but Rust does, which is one of its benefits.
- phibz 7y agoIn your rust example could you give "a" a lifetime that is the same as the lifetime of the impl Fn that foo() returns? That way "a" lives as long as the impl Fn.
- phibz 7y agoI tried changing a to a reference and doing: fn foo<'a>(a: &'a f64) -> impl Fn(f64) -> f64 + 'a { .... but it doesn't work