#capture #closures #syntax #modern #lambda #move #cpp

no-std capture-it

Modern c++-ish capture syntax for rust

9 unstable releases (3 breaking)

0.4.3 Jul 18, 2023
0.4.2 Jul 18, 2023
0.4.1 Apr 9, 2023
0.3.1 Apr 2, 2023
0.1.1 Mar 14, 2023

#282 in Rust patterns

Download history 6/week @ 2024-01-26 1/week @ 2024-02-02 3/week @ 2024-02-16 12/week @ 2024-02-23 17/week @ 2024-03-01 27/week @ 2024-03-08 19/week @ 2024-03-15 28/week @ 2024-03-29 22/week @ 2024-04-05 5/week @ 2024-04-12

55 downloads per month
Used in 3 crates (2 directly)

MIT license

38KB
541 lines

capture-it   crates.io

See example

For detailed documentation, see capture_it::capture

Usage

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);
    }

Bonus

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(&times.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();

Trivia

Other closure crates use a more intuitive capture syntax...

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.

No runtime deps