Types of Fn traits and their cability

I would just post my conclusions from the automatic Copy/Clone trait implementations for closures.

  • If a closure is move and at least one captured variables does not implement Copy/Clone, we get a FnOnce without Copy/Clone.
  • If a closure is move and all captured variables implements Copy/Clone, we get a FnOnce with Copy/Clone accordingly. (Technically I should distinguish Copy and Clone but the detail is unimportant here)
  • If a closure is not move and it attempts to mutate its captured variables, we get a FnMut and so FnOnce
  • If a closure is not move and there is no attempts to mutate its captured variables, we get a Fn and implicitly get FnMut + FnOnce + Copy + Clone for free (because all captured variables are by reference and implements Copy).

So far so good. But when we jump into another world that we didn’t use closures but attempts to implement those traits manually, we can implement something that does not have a closure correspondence:

  1. FnMut + Copy/Clone without Fn
  2. Fn without Copy/Clone

Case 1 is pretty useless to me. If we need a FnMut, we want to call it to mutate some internal status, and so if we attempt to copy this thing, we are copying something mutable and this is always confusing in Rust’s strict semantic.

I have less confident on case 2, but looks still strange to me. In such a case we have an callable object that does not change itself when called, but it cannot be copied/cloned. I can hardly think of a case that this could be useful.

So my question is, shall we restrict these traits to not allowing those implementations? This will be a breaking change, but I just doubt that how many existing code relying on this.

Technically, avoiding case 1 is hard unless we have something unusual for just this case. But case 2 can be simply done by making Fn inherits Copy. Case 1 can be done as a lint though, as it is unlikely developers will explicitly doing this.

1 Like

I agree that FnMut + Copy is a little weird, but FnMut + Clone could be useful. As an example:

#[derive(Clone)]
struct Counter(u32);

impl FnOnce<()> for Counter {
    type Output = u32;
    extern "rust-call" fn call_once(self, (): ()) -> u32 {
        self.0
    }
}
impl FnMut<()> for Counter {
    extern "rust-call" fn call_mut(&mut self, (): ()) -> u32 {
        let result = self.0;
        self.0 += 1;
        result
    }
}

The Counter has state, but you can use clone() to capture the current state, while allowing the original Counter to continue to be used.

1 Like

This should not be limited to FnOnce. A closure which implements Fn, FnMut, or FnOnce still does so with or without move. The difference is only in how the captured variables are stored -- normally this is determined by the way a particular variable is used, whether by &, by &mut, or by value (consumed, iff non-Copy).

With move, it's forced to always capture by value, but the body of the closure doesn't have to consume those values. Fn is called with &self, FnMut with &mut self, and FnOnce with self. A move Fn is still a perfectly reasonable thing, but still might not be possible to Copy.

4 Likes

OK, I see. You corrected me with something I didn't get right before, thank you.

But then I have other concerns.

I was one of the author of the Y combinator in Rosetta Code. When I was working on this I noticed that Rust have something that other languages does not, namely in this case, the distinguish of Fn, FnMut and FnOnce and I feel like this is a good thing.

However I figured out later that I can only use Fn in a fixed point combinator and we have a very good reason: an FnOnce can be called only once, so no recursion will happen to it. An FnMut have a mutable reference to itself when called, and in Rust you cannot keep another mutable reference for later calls.

Until recently I noticed that we are able to have closures that are FnOnce + Clone, and then I can relax the restriction from only Fn to FnOnce + Clone.

However, in today's stable rust we are not able to constraint on both Fn and FnOnce + Clone, as they are overlapping sets. Of cause, I know we can use nightly and enable a feature to make it possible, but I just feel like it seems not making any sense to have an Fn that is not Copy.

If Fn implies FnOnce + Copy, I can simply write the code in today's Rust and have constraint on FnOnce + Clone. So this is why I am asking here.

FnOnce + Clone can also be wrapped with an Fn closure, like:

fn wrap<F, T, R>(f: F) -> impl Fn(T) -> R
where
    F: FnOnce(T) -> R + Clone,
{
    move |x| f.clone()(x)
}

Here’s an impl Fn() that cannot be Copy:

fn assert_copy<C: Copy>(_: C) {}
fn assert_fn<F: Fn()>(_: F) {}

struct Opaque(());

fn main() {
    let o = Opaque(());
    let f = move || { &o; };
    assert_copy(f); // error, `f` is not `Copy` because `Opaque` is not `Copy`
    assert_fn(f);
    // EDIT: both of these `drop` are errors as well
    // the errors are just masked by `assert_copy` for some reason
    drop(f); // error, `f` was moved into `assert_fn`
    drop(o); // error, `o` was moved into `f`
}
1 Like

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