Crates.io | capture-it |
lib.rs | capture-it |
version | 0.4.3 |
source | src |
created_at | 2023-03-14 15:32:57.282447 |
updated_at | 2023-07-18 13:33:24.199757 |
description | Modern c++-ish capture syntax for rust |
homepage | |
repository | https://github.com/kang-sw/capture-it |
max_upload_size | |
id | 809936 |
size | 50,140 |
See example
For detailed documentation, see capture_it::capture
Creates closures with a syntax similar to modern C++'s lambda capture rules. The first argument to
the capture!
macro is an array listing the arguments to be captured by the closure, and the second
argument specifies either an 'async move' block or a 'move' closure. (to more explicitly indicate
that the move
closure is used, a compile-time error is raised for any async or closure
function missing the move
tag).
The following example demonstrates how to create a generator closure by capturing an arbitrary expression (=0
) with the index
identifier.
use capture_it::capture;
// You can capture an expression, as we do in c++'s lambda capture.
//
// Any identifier prefixed with *(asterisk) declared as mutable.
let mut gen = capture!([*index = 0], move || {
index += 1;
index
});
assert!((gen(), gen(), gen(), gen(), gen()) == (1, 2, 3, 4, 5));
Since the function arguments of the capture
macro must use the move
closure, reference captures must be explicitly listed; they are represented by the &
or &mut
prefix, as in normal rust syntax.
use capture_it::capture;
let mut num = 0;
let mut gen = capture!([&mut num], move || {
*num += 1;
*num
});
assert!((gen(), gen(), gen(), gen(), gen()) == (1, 2, 3, 4, 5));
The capture!
macro calls Clone::clone
for every argument passed in, by default. This is a more
ergonomic way to create closures.
use capture_it::capture;
use std::sync::{Arc, Mutex};
let arc = Arc::new(Mutex::new(0));
// From this ...
std::thread::spawn({
let arc = arc.clone();
move || {
*arc.lock().unwrap() += 1;
}
});
// To this
std::thread::spawn(capture!([arc], move || {
*arc.lock().unwrap() += 1;
}));
// The naive spin wait ...
while Arc::strong_count(&arc) > 1 {
std::thread::yield_now();
}
assert_eq!(*arc.lock().unwrap(), 2);
This macro is particularly useful when you need to pass multiple Arc
instances through Clone
to different closures. Take a look at the following example to see how it simplifies traditional block capture.
use capture_it::capture;
use std::sync::Arc;
let arc = Arc::new(());
let arc2 = arc.clone(); // let's just think these are all different variables
let arc3 = arc.clone();
let arc4 = arc.clone();
let while_strong_count = |arc: &Arc<()>, pred_continue: fn(usize) -> bool| {
while pred_continue(Arc::strong_count(arc)) {
std::thread::yield_now();
}
};
// Before, when you have to capture variables by copy ...
std::thread::spawn({
let arc = arc.clone();
let arc2 = arc2.clone();
let arc3 = arc3.clone();
let arc4 = arc4.clone();
move || {
while_strong_count(&arc, |x| x >= 8);
// we have to explicitly capture them.
drop((arc2, arc3, arc4));
}
});
// Then, we can write same logic with above, but in much more concise way
std::thread::spawn(capture!([arc, arc2, arc3, arc4], move || {
while_strong_count(&arc, |x| x >= 12);
// `capture!` macro automatically captures all specified variables into closure,
// thus, we don't need to explicitly capture them.
// drop((arc2, arc3, arc4));
}));
assert!(Arc::strong_count(&arc) == 12);
// as all variables are captured by clone, we can still owning `arc*` series
drop((arc2, arc3, arc4));
while_strong_count(&arc, |x| x > 1);
All variables other than those specified in the capture list follow the normal closure rules for rust, so if you need to take ownership of a variable, simply remove its name from the capture list.
use capture_it::capture;
use std::sync::Arc;
let cloned = Arc::new(());
let moved = cloned.clone();
std::thread::spawn(capture!([cloned], move || {
// Explicit 'move' capture
drop(moved);
}));
// 'moved' was moved. So we cannot use it here.
// drop(moved);
while Arc::strong_count(&cloned) > 1 {
std::thread::yield_now();
}
Asynchronous blocks follow the same rules.
use capture_it::capture;
use futures::{SinkExt, StreamExt};
let (tx, mut rx) = futures::channel::mpsc::unbounded::<usize>();
let task1 = capture!([*tx], async move {
// `move` is mandatory
for val in 1..=3 {
tx.send(val).await.unwrap();
}
});
let task2 = capture!([*tx], async move {
for val in 4..=6 {
tx.send(val).await.unwrap();
}
});
drop(tx); // we still have ownership of tx
task2.await;
task1.await;
for val in (4..=6).chain(1..=3) {
assert_eq!(rx.next().await.unwrap(), val);
}
The capture
macro contains several syntactic sugars. For example, if you want to capture the type
&str
as the corresponding ToOwned
type, String
, you can apply the Own(..)
decorator.
use capture_it::capture;
let hello = "hello, world!";
let mut gen = capture!([*Own(hello), *times = 0], move || {
times += 1;
hello.push_str(×.to_string());
hello.clone()
});
assert_eq!(gen(), "hello, world!1");
assert_eq!(gen(), "hello, world!12");
assert_eq!(gen(), "hello, world!123");
The Weak
decorator is used to capture a downgraded instance of Arc
or Rc
.
use capture_it::capture;
use std::rc::Rc;
use std::sync::Arc;
let rc = Rc::new(());
let arc = Arc::new(());
let closure = capture!([Weak(rc), Weak(arc)], move || {
assert!(rc.upgrade().is_none());
assert!(arc.upgrade().is_some());
});
drop(rc); // Let weak pointer upgrade of 'rc' fail
closure();
The Some
decorator is useful to mimic FnOnce
in the FnMut
function.
use capture_it::capture;
let initial_value = ();
let mut increment = 0;
let mut closure = capture!([*Some(initial_value), &mut increment], move || {
if let Some(_) = initial_value.take() {
// Evaluated only once, as we can take out `initial_value` only for single time...
*increment = 100;
} else {
*increment += 1;
}
});
closure();
closure();
closure();
assert_eq!(increment, 102);
Any other function call with single argument can be used as a decorator. For example, the normal
clone representation of the capture
macro is replaced by Clone::clone(var)
.
use capture_it::capture;
let clone1 = std::sync::Arc::new(());
let clone2 = clone1.clone();
// following capture statement, `clone1` and `Clone::clone(&clone2)` behave equivalent.
let closure = capture!([clone1, Clone::clone(&clone2)], move || {
drop((clone1, clone2)); // Explicit drop will make this closure `FnOnce`
});
closure();
Alternatively, you can capture the return value of a function called on self
as the name of its
variable. Function calls can contain parameters, but there are some restrictions; for example,
re-chaining to a function's return value will not work (var.foo().bar()....
). Only one function
call is allowed.
Decorators are useful for capturing simple type changes; if you want to capture complex expressions, it's best to use the assignment syntax of a=b
.
use std::{rc::Rc, sync::Arc};
use capture_it::capture;
let arc = Arc::new(());
let rc = Rc::new(());
let weak_arc = Arc::downgrade(&arc);
let weak_rc = Rc::downgrade(&rc);
drop(arc);
// The return value of `.upgrade()` will be captured as of its name.
let closure = capture!([weak_arc.upgrade(), weak_rc.upgrade()], move || {
assert!(weak_arc.is_none());
assert!(weak_rc.is_some());
});
closure();
For example, the (move a, ref b, clone b, ...)
grammar in the closure
crate can express closure parameters more intuitively.
On the other hand, the *
prefix to express the mutability of capture-it
is unintuitive and hard to understand - why did we do it this way?
Introducing new grammars is a very tempting option, but by default, most of these attempts are poorly understood by the rustfmt
utility.
Since closure macros typically pass the function body as a macro argument, a fairly long body can lose the benefit of the formatter if the rustfmt
parser fails to parse the macro argument.
On the other hand, the capture_it::capture
macro is a perfectly valid rust syntax (at least syntactically) that simply passes an array and a single function block as macro arguments. (Also, a capture list wrapped in [square brackets] can be used in a similar sense to C++.)
So any capture and function block you define in the capture_it::capture
macro can be formatted by rustfmt
, which is something I personally find quite important.