Generics unbind lifetimes and I dislike that

Hi!
I recently discovered a problem in my code which I don't know how to escape.
Basically, my structure behaves similar to this:

// this makes an anonymous struct which takes the `T`
fn printer<T: Debug>() -> impl FnOnce(T) {
    |val| { println!("{val:?}"); }
}

fn take_binded(_: impl for<'a> FnOnce(&'a i32)) { .. }

Now writing the printer without the generic T makes it able to being passed into take_binded.
However, because there's the generic involved, it tries to collapse the lifetime and I instead get

error: implementation of `FnOnce` is not general enough
 --> src/main.rs:5:5
  |
5 |     take_binded(printer());
  |     ^^^^^^^^^^^^^^^^^^^^^^ implementation of `FnOnce` is not general enough
  |
  = note: `impl FnOnce(&'2 i32)` must implement `FnOnce<(&'1 i32,)>`, for any lifetime `'1`...
  = note: ...but it actually implements `FnOnce<(&'2 i32,)>`, for some specific lifetime `'2`

As I've already played around with some basic trait shenanigans to overcome this, I don't think it's possible to make this work (except there's a suuuper magic trick for this).

Tho I wanted to ask if there's any reason logical-wise which should disallow this or if there just wasn't yet made the effort to "track bindings" of lifetimes with generics.

Thanks :smiley:

PS: for playing around with the issue playground

1 Like

T represents a single type, and there is no single &i32 type for all lifetimes.[1]

Rust doesn't have "generic type constructors" to allow something like

//         vvvvv Some type constructor generic over... whatever.
fn printer<T<..>: Debug>() -> impl FnOnce(T<..>) 
// (Lifetimes are all that a higher-ranked `FnOnce` could support so far.)

If we get something like that, I doubt it would be invisible (look like your OP / be always or implicitly present in generics). If we get something close to it, it won't be for many years. I doubt it's a simple feature.

The closest serious proposals that come to mind are generic variadics, but I haven't checked to see how applicable those are to this use case.


  1. Even if there was, that would mean you want a FnOnce(for<'a> &'a i32) and not a for<'a> FnOnce(&'a i32). ↩︎

By the way, a workaround for your playground is

    test(|i| { printer()(i) });
    test(|i| unbind(|_| {})(i));

I thought of another possible future feature: Opaques which are higher-ranked over types.

// Perhaps some day we'll get this and it will also meet a
// `for<'a> FnOnce(&'a i32)` bound
fn printer() -> impl for<T: Debug> FnOnce(T) {
    |val| { println!("{val:?}"); }
}

Probably requires a more general "higher-ranked over types" support. There's some experimental support for non-lifetime binders, but the experiment doesn't cover higher-ranked types or opaques, just where clauses (think GAT bounds).[1]


  1. It's also an incomplete feature. ↩︎

You an also change printer to return impl for<'a> FnOnce<&'a T> or relax the for<'a> on F in take_binded, so that it simply needs to work for a single lifetime (i.e., fn take_binded<'a, F: FnOnce(&'a i32)>(_ :F){}. Those workarounds may cause other issues in the actual code though.

1 Like

Those are all great ideas, but sadly I can't say that T will always be of type &'_ T and even tho I already use the non_lifetime_binder feature, I don't think that's where I'm heading as my real code doesn't use FnOnce but my own trait.
However I've now come up with a "trampoline" implementation of the trait which explicitly covers the lifetime in the type and then refers to that impl everywhere.