[Feature Request] Add "clone" closure

Move closures are great, but for things like reference counters and channels it can get a bit weird/tedious.

The idea is to just have a "clone" closure that clones values rather than moving them or taking references to them, so that this:

    for _ in 0..NUM_WORKERS {
        let results_tx_clone = results_tx.clone();
        let jobs_rx_clone = jobs_rx.clone();
        handles.push(thread::spawn(move || {
            worker(jobs_rx_clone, results_tx_clone)
        }));
    }

becomes this:

    for _ in 0..NUM_WORKERS {
        handles.push(thread::spawn(clone || {
            worker(jobs_rx, results_tx)
        }));
    }

It feels like we have this really neat syntax for "closure types," but it's really not used as much as it could be. This could be a way to expand on that.

3 Likes

Just a note: clone || { ... } can't be a closure, because it's a logical or between the binding clone and the value of the block expression. The reason move || works is because move is a keyword. The same applies with closure arguments, just with bitwise or.

I also personally think this gets too close to implicit cloning for my tastes, and runs the risk of accidental unnecessary clones of other captures.

Note that also you don't have to rename the clones to capture; you can just rebind the same name. It's actually rather common to list explicit captures as

{
    let x = x.clone();
    let y = y.clone();
    move || {
        let _ = (x, y);
        // ...
    }
}

when explicit captures are useful for clarity.

That said, there is also some support for a proper explicit captures list (to be more explicit and avoid accidental captures increasing closure size), and such having a way to capture by cloning doesn't seem impossible.

18 Likes

I wonder if this can be combined with a "cheap clone" trait to make Arc and other cheap-clone-trait-marked types behave more or less like Copy types in closures. I think that would satisfy most use cases, and do the right thing, without a need for a new closure syntax.

3 Likes

I guess people who care a lot about performance would be unhappy about that, because cloning an Arc isn't free, it has synchronization overhead.

Isn't free compared to what?

I think it's a zero-cost abstraction, because your choices are taking by temporary reference with all limitations it entails, or use a move closure and current verbose syntax to do the cloning anyway. Typing let foo = Arc::clone(&foo); has the same performance as the compiler inserting the same if needed.

2 Likes

Yes. It would be weird if syntactic sugar performed differently than the code it desugars to. But that's not my point. As you mentioned, it's sometimes possible to use a &T instead of an Arc<T>, and in these situations cloning an Arc<T> is not zero-cost compared to just borrowing it.

It's good to be explicit when it helps someone reading the code to understand what the code does, or why it performs the way it does.

3 Likes

I'm not proposing to remove by-ref closures, only to change a compilation error to the exact same solution that user would write anyway, and only in cases when there's nothing interesting nor surprising about it.

3 Likes

Yes, I'm aware of that. Still, many people prefer this to be explicit rather than implicit.

2 Likes

What if there was a more general escape hatch for closures, to indicate that a particular expression should be eagerly evaluated and only it's result captured.

I can't think of a good syntax, so as a silly placeholder lets say && { ... } is the escape .

Then you might write:

    for _ in 0..NUM_WORKERS {
        handles.push(thread::spawn( || {
            worker( && { jobs_rx.clone() }, && {results_tx.clone()} )
        }));
    }

or perhaps:

    for _ in 0..NUM_WORKERS {
        handles.push(thread::spawn( || {
            worker( && { Arc::clone(&jobs_rx) }, && { Arc::clone(&results_tx) } )
        }));
    }

The most obvious use case is clone(), but this way the feature isn't tied directly to the clone trait; and can also be used with either move closures or copy ones.

4 Likes

Related work: the glib bindings crate offers a clone! macro with the same motivation. In gobject, all objects are automatically reference-counted so there is a lot more need for this than in usual Rust. Nevertheless, I've wished this to be in the standard library more than once.

It would also be worth-while evaluating whether weak clones are common enough as a use case or not.

6 Likes

Well, maybe just use tuple clone?

    for _ in 0..NUM_WORKERS {
        let (results_tx, jobs_rx) = (results_tx, jobs_rx).clone();
        handles.push(thread::spawn(move || {
            worker(jobs_rx, results_tx)
        }));
    }
1 Like

That won't work, since it will move results_tx and jobs_rx.

1 Like

Oops, yes. Then maybe something like this?

trait Cloned {
    type Output;
    fn cloned(self) -> Self::Output;
}
impl<'x, A, B> Cloned for (&'x A, &'x B)
where
    A: Clone,
    B: Clone,
{
    type Output = (A, B);
    fn cloned(self) -> Self::Output {
        (self.0.clone(), self.1.clone())
    }
}


for _ in 0..NUM_WORKERS {
        let (results_tx, jobs_rx) = (&results_tx, &jobs_rx).cloned();
        handles.push(thread::spawn(move || {
            worker(jobs_rx, results_tx)
        }));
    }
1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.