1 unstable release
0.1.0 | Nov 26, 2021 |
---|
#17 in #directive
26KB
417 lines
More Powerful Closure Captures
This crate provides simple macros letting you express more powerful closure captures. For example, you can capture the clone of a value:
use std::rc::Rc;
let my_val = Rc::new(1);
captures::capture!(clone my_val, move || {
// `my_val` is cloned here!
});
You can also capture arbitrary expressions and override the Edition-2021 capture semantics. Best of all, you can even specify that your closure should not capture any variables outside the ones you've listed:
let a = 1;
let b = 2;
captures::capture_only!(clone a, move || {
a + b // errors: `b` is unknown
})
Consult the full documentation for the details.
lib.rs
:
Provides two macros for more powerful closure captures.
Background
Closures in Rust, despite being extremely powerful, do not offer many options for modifying the
way in which they capture their output. A particular pain point is often needing to .clone()
an Arc<T>
or Rc<T>
for the closure to capture. This pattern does not compile:
fn needs_static<T: FnOnce() -> i32 + 'static>(f: T) -> i32 {
f()
}
let local: Rc<i32> = Rc::new(1);
// Try and capture a clone of the `Rc`
let mut f = || {
let in_closure = local.clone();
*in_closure.as_ref()
};
// `f` is not `'static`!
assert_eq!(needs_static(f), 1);
// `local` has not been captured!
assert_eq!(*local.as_ref(), 1);
That's because when writing local.clone()
in the body of the closure, that clone
call is not
executed until the closure is called; this means that the closure is actually capturing a
&local
and so it's not 'static
! Making f
a move
closure does not fix this, since then
local
will be captured by value, and the later local.as_ref()
statement will fail. What we
want instead is for the .clone()
to be executed when the closure is created:
fn needs_static<T: FnOnce() -> i32 + 'static>(f: T) -> i32 {
f()
}
let local: Rc<i32> = Rc::new(1);
// Actually capture a clone of the `Rc`
let cloned = local.clone();
let f = move || {
let in_closure = cloned;
*in_closure.as_ref()
};
// `f` is now `'static`!
assert_eq!(needs_static(f), 1);
// `local` has not been captured!
assert_eq!(*local.as_ref(), 1);
Usage
The captures::capture
and captures::capture_only
macros are invoked with a comma-seperated
list of "capture directives" and finally a closure expression. One example of a capture
directive is the clone x
directive, which indicates that a clone of x
should be captured in
place of x
. As such, the example above can be re-written to:
use captures::capture;
fn needs_static<T: FnOnce() -> i32 + 'static>(f: T) -> i32 {
f()
}
let local: Rc<i32> = Rc::new(1);
// Actually capture a clone of the `Rc`
let f = capture!(clone local,
move || {
let in_closure = local;
*in_closure.as_ref()
}
);
// `f` is still `'static`!
assert_eq!(needs_static(f), 1);
// `local` has not been captured!
assert_eq!(*local.as_ref(), 1);
Capture Directives
These capture directives are currently supported:
clone x
captures a clone ofx
.with x = expr
captures a valuex
that is computed fromexpr
.all x
captures all ofx
. Beginning in Rust 2021, writingx.y
in your closure would lead to only they
field ofx
being captured. Specifyingall x
causes all ofx
to be captured instead. This does not influence whetherx
is captured by value or by reference - if the closure is amove
closure, it will still be captured by value, and if it is a non-move
closure, the compiler's standard inference algorithm is allowed to make the decision.
To avoid surprises and compilation errors, if you specify a clone
or with
directive, then
this macro will turn your closure into a move closure if it was not one already. Because of
this, if your closure is a move
closure - either because you explicitly marked it as such or
because you used a with
or clone
directive - then you may additionally specify these
directives:
ref x
capturesx
by immutable reference.ref mut x
capturesx
by mutable reference.
The x
in all of these directives must simply be the name of a local variable. Some more
complicated things may be supported in the future. There is at the moment also no support for
combining directives. I will add this once I figure out a pretty and consistent way to do it.
Mutability
In Rust, captured variables that are captured by value inherit the mutability of the value they reference. For example,
let a = 1; // immutable
let _ = move || {
a += 1;
a
};
does not compile, but if a
is marked as mutable
let mut a = 1; // now mutable
let _ = move || {
a += 1;
a
};
it does.
Unfortunately, this crate does not have the necessary information to reproduce this behavior in
general. clone
and with
directives create new variables for which it is not clear what their
mutability should be. The current policy is for all of them to default to immutable. This may be
changed in the future (obviously respecting semver) if it is determined that this is not the
best option. If you do want these values to be mutable, you can request that by prefixing the
variable with a mut
. For example,
let mut v = vec![1, 2]; // despite being mutable here
let _ = capture!(clone v, || {
v.push(3); // we cannot push to `v`, since it is not mutable
v
});
We can fix this via:
let mut v = vec![1, 2];
let _ = capture!(clone mut v, || {
v.push(3); // we can push now
v
});
This will still emit a warning because the mutability of the variable v
outside the closure is
unused. Writing instead let v = vec![1, 2];
would continue to compile and the warning would not
be emitted.
all
directives are not affected by this. Variables captured under such a directive, if
captured by value, correctly inherit their mutability. As such, the mut
prefix is not
supported on these directives.
capture_only
The capture_only
macro behaves exactly like the capture
macro, with the exception that it
additionally prevents any variables that do not have an associated capture directive from
being captured. For example,
let a = 1;
let mut b = 10;
let mut f = capture_only!(all a, || {
b += 1; // error
a + 1
});
assert_eq!(f(), 2);
assert_eq!(b, 11);
does not compile, with an error message indicating that there is no local variable b
.
Switching capture_only
to capture
would allow the above code to compile. If you would like
to indicate that b
may also be captured, but do not want to add any restrictions on how, you
can add an all
directive:
let a = 1;
let mut b = 10;
let mut f = capture_only!(all a, all b, || {
b += 1; // compiles
a + 1
});
assert_eq!(f(), 2);
assert_eq!(b, 11);
Dependencies
~1.5MB
~37K SLoC