Why a lambda is refused in an Option or even by giving it a name?

I found the code in another form. The code is pasted directly below:

struct P<'a> {
    v: Vec<&'a String>,
}

impl<'a> P<'a> {
    fn test<F>(&mut self, f: Option<F>)
    where
        F: FnOnce(&mut P<'a>) + 'a,
    {
        if let Some(f) = f {
            f(self);
        }
    }

    fn test2<F>(&mut self, f: F)
    where
        F: FnOnce(&mut P<'a>) + 'a,
    {
        f(self);
    }
}

fn main() {
    let s = "11".to_string();
    let mut p = P { v: vec![] };

    // Fail 1
    // p.test(Some(|this: &mut P<'_>| {
    //     this.v.push(&s);
    // }));

    // Fail 2
    // let lambda = |this: &mut P<'_>| {
    //     this.v.push(&s);
    // };
    // p.test2(lambda);

    // Ok
    p.test2(|this: &mut P<'_>| {
        this.v.push(&s);
    });

    println!("{:?}", p.v);
}

It totally confused me, I don't know why the first 2 was refused

Option<F> is not FnOnce, even when F: FnOnce, so why do you expect this to work?

Dont understand, it does not require Option<F> : FnOnce
There are 2 test functions, namely test and test2, you might have seen them wrong.

When you call it as test2(Some(...), test2 is actually test2<Option<...>>, not test2<{closure type}>.

There is no test2(Some(...)), it is test(Some(...))

1 Like

Your test and test2 require a higher-ranked trait bound on F, which in turn requires the compiler to infer whether your closures are generic over which of their lifetimes. The compiler is notoriously bad at doing this kind of inference (though to be fair it might be undecidable in the general case). One case where it generally successfully infer it is when you're directly passing the closure to a function with that bound, like in the case it compiles for you, because the compiler can immediately see it must satisfy the given bound. However this "trick" is pretty fragile and can break the moment you introduce any kind of indirection, like when storing the closure in a local variable first or wrapping it in other types. You can fix these cases by adding an helper function that only enforces the problematic bound. Bonus: this will work even when not specifying the argument type.

2 Likes

It is not because of test only, without test the code still is unaccepted

struct P<'a> {
    v: Vec<&'a String>,
}

fn test() {
    let s = "11".to_string();
    let mut p = P { v: vec![] };

    let lambda = |this: &mut P<'_>| {
        this.v.push(&s);
    };
    println!("{:?}", p.v);
}

Edit:
Well, this code indeed is unsound, only 'static can fit all P<'_>
The problem seems to be

let lambda = |this: &mut P<'_>| {
        this.v.push(&s);
    };

is unsound as a whole, but

p.test2(lambda);

only needs the valid part of it

Because lambda is contravariant, this means anylife time longer than p is valid, and obviously only s: 'static can be compiled.

In the absence of test2 or wrap or another immediate context with a Fn-trait bound, the compiler will just use it's default inference -- the one that is notoriously bad with regards to higher-rankedness. test isn't making things worse, it's just failing to make things better.

Your terminology is confusing. Or confused; non-compiling closures aren't unsound, and the compiler doesn't somehow partially compile closures by only picking the sound parts, whatever that means.

The compiler sees:

// wildcard lifetimes --vv------vv   capture ------vv
let lambda =    |this: &  mut P<'_>| { this.v.push(&s) };

And tries to do something like this:

struct Closure<'a>(&'a String);
impl<'a> Closure<'a> {
    //                    vv------vv
    fn call(&self, this: &  mut P<'_>) {
        this.v.push(self.0);
    }
}

And then errors when that fails borrow chek.

But with test2 or wrap, it bases its implementation on the bound and instead does something like

//                  vv-------------+
struct Closure<'a>(&'a String); // |
impl<'a> Closure<'a> {//           |
    //                            vv
    fn call(&self, this: &  mut P<'a>) {
        this.v.push(self.0);
    }
}

which works.

You're talking about the signature of the Fn* implementation as if it was the variance of the closure or such, I think, which is a bit off. And variance isn't the problem.

// 'r and 'a represent single, concrete lifetimes

fn(&'r mut P<'a>)  // 'r position: contravariant, 'a pos.: invariant
fn(&   mut P<'a>)  // outer lifetime: higher ranked, 'a pos.: invar.
fn(&   mut P<'_>)  // both lifetimes: higher-ranked

// In all cases, inner lifetime: outer lifetime is implied

The problem isn't contravariance -- and the inner lifetime is always invariant in the signature. The problem is higher-rankedness of the inner lifetime. The last signature can be invoked as a fn(&'static mut P<'static>).

Covariance of the lifetime of &String means a &'static String capture would work with any of those signatures.

Even if the closures in question can be covariant over their capture, it wouldn't be useful in this case, where you need a specific lifetime in invariant position in the call signature.


However! What if we had bounds like this?

// 'a is the lifetime of the captured `&'a String`
for<'s where 'a: 's> fn(&mut P<'s>)

That would be compatible with the capture similar to how the fully higher-ranked signature is compatible with &'static String. The lifetime of the capture is an upper bound on the lifetimes of P<'_> which can be mutated.

We can emulate those conditional higher-ranked bounds with implied bounds.

fn wrap<'a, F>(f: F) -> F
where
    F: for<'s> Fn(&mut P<'s>, [&'s &'a (); 0]),
    // Implied under binder:   ^^^^^^^ 'a: 's

No reason for it in the OP, and no advantage since you still need the wrap and have to add dummy arguments everywhere, but it works.

It'd be nice if there was a way to get the compiler to emit the corresponding implementation without implied bounds, but we'd need at a minimum some syntax like for<'s where 'a: 's> for the wrap approach.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.