"return type references unconstrained lifetime" with GATs

trait Trait {
    type Gat<'a>;
}

// Doesn't compile: `'a` in fn ptr return type is unconstrained
fn test<T: Trait>(_: for<'a> fn(T::Gat<'a>) -> &'a i32) {}

The above doesn't compile, because T::Gat<'a> is not guaranteed to reference 'a, which leaves the return type of the fn pointer unconstrained.

But that return type, &'a i32, is covariant with respect to 'a. So, could this be allowed to compile, by requiring 'a to have the longest lifetime possible when it's not otherwise constrained? E.g., the return type of for<'a> fn(T::Gat<'a>) -> &'a i32 would be required to be &'static i32 if T::Gat<'a> doesn't constrain 'a.

Analogously, for for<'a> fn(T::Gat<'a>) -> fn(&'a i32) ('a in contravariant position in return type), if T::Gat<'a> doesn't constrain 'a, 'a could be required to be 'empty.

1 Like

Interesting proposal. The part about some 'empty lifetime might involve too many novel type system concepts for too little benefit, so leaving out that part should probably be an option.

I see potential for confusion from the fact that in such a setting, the type for<'a> fn(T::Gat<'a>) -> &'a i32 might not implement the trait bound Fn(T::Gat<'a>) -> &'a i32 (for non-static lifetimes 'a). Of course, you can still use it like such a function because coercing the actual return type (&'static i32) accordingly is possible.

Also, if you want to make such a function call in a generic setting, the compiler might need to argue about an underdefined return type, being either &'a i32 or &'static i32 depending on the trait implementation; and one would expect to be able to actually obtain at least an &'a i32 even in such a generic setting. Maybe some special-case logic is even necessary, IDK, but it feels somewhat complex.

Also you'd probably expect to be able to coerce a for<'a> fn(T::Gat<'a>) -> &'a i32 into a concrete type fn(T::Gat<'a>) -> &'a i32 for some choice of 'a, which should be sound due to variance.

But then there's the issue that supporting for<'a> dyn Fn(T::Gat<'a>) -> &'a i32 in a similar manner might be more tricky. In particular, coercing something like Box<for<'a> dyn Fn(T::Gat<'a>) -> &'a i32> into Box<dyn Fn(T::Gat<'a>) -> &'a i32> for some concrete lifetime 'a would either require special case logic for the Fn traits or a new notion of variance in associated types of a trait, because as of now, Box<dyn Fn(T::Gat<'a>) -> &'a i32> is just syntactic sugar for Box<dyn Fn<(T::Gat<'a>,), Output = &'a i32>>, and the associated types in such trait object types are always invariant, so coercing e. g. Box<dyn Fn() -> &'static i32> to Box<dyn Fn() -> &'a i32> (for nonstatic lifetime 'a) is currently not possible (though arguably it should be sound to allow such coercions [1]).

Of course one could also accept the slight inconsistency of only allowing such new types for fn pointer types, but not for Fn trait object types; after all, there's already significant differences between the two, such as the variance of fn pointer type argument types and return types.


  1. but as mentioned above, that would require magic special-casing of Fn* traits or a new notion of variance in associated types ↩ī¸Ž

1 Like