[Replying to @emersonford's post here since it seems more appropriate]
nice post @emersonford
Regarding:
What's not immediately clear to me though is why this is so specific to auto traits + generators. Even if you don't need to prove Send for the generator, don't you still need to prove that Trait is implemented for fn(&'static ()) inside of the generator interior? I.e., why does
It's specific to:
- generators, because they're one place where the lifetimes used by the resulting type/enum that makes the
Future
are erased: Rust does not have the tools to express self-referential lifetimes, so to avoid being unsound with the "made up" lifetimes that can end up populating the compiler-generated self-referential datastructure backing generators / async
, the compiler straight up erases them somehow so as to prevent lifetime-specific impls from applying (since in certain scenarios this could be unsound);
- auto-traits since they:
- leak through
-> impl Trait
/ existential trait abstractions (that is, contrary to other traits which are just stated explicitly as part of the -> impl Trait
contract, so that they can be checked within the right context, auto-traits are not enforced there and then, and instead checked later on, in a context with erased lifetimes;
- in a structural manner, meaning it delegates to each generator capture implementing the trait, as you mentioned, which brings us to
1.
Now, the culprit triggering this is indeed the associated type, <T as Trait>::Assoc
. At that point, even if the data structure itself (Foo
in my example) unconditionally requires T : Trait
, the trait checker will nonetheless try to prove this again, and this time with the added difficulty of having erased lifetimes, even the 'static
lifetime.
Workaround for library authors
Is then not to use associated types.
That is, replacing:
struct Foo<T : Trait>(
T::Assoc,
);
with:
struct Foo<T : Trait<Assoc = R>, R = <T as Trait>::Assoc>(
R,
::core::marker::PhantomData<T>,
);
that is, we introduce a new type R
, which defaults to T::Assoc
, and we also require that T::Assoc
be equal to R
. All in all, inference should work just as well, and basically anything else too, but for the type now containing a fully standalone R
type. This will avoid the requirement to check T : Trait
within an erased-lifetime scenario.
Illustration
Using .flat_map(|x| x)
instead of .flatten()
dodges the issues, even though they are semantically equivalent. The difference is that flatten
does mention <I as Iterator>::Item
, whereas flat_map
uses a fresh U
type parameter which is only constrained to be F::Output
for extra trait impl
ementations.
Potentially interesting?
Since using struct X(fn(&'static ());
dodges the issue too (since the X
"type constructor" is no longer a "type with a movable lifetime within in"), I got curious about trying to formalize stuff using hand-rolled type constructors, i.e., HKT
s:
trait WithLifetime<'a> { type T; }
type Feed<'a, T : WithLifetime<'a>> = T::T;
trait HKT = for<'a> WithLifetime<'a>;
type A = dyn for<'a> WithLifetime<'a, T = fn(&'static ())>;
type B = dyn for<'a> WithLifetime<'a, T = fn(&'a ())>;
Here, we have A
and B
both being fully-fledged types, but which happen to express a form of <'a>
-genericity, which could be expressed in the following pseudo-code, sort to speak:
type A<'a> = fn(&'static ());
type B<'a> = fn(&'a ());
And doing that, we could then replace struct Foo<T>(<T as Trait>::Assoc) where T : Trait;
definition with:
/// Idea: replace `T` with `Feed<'static, T_>`, for some `T_ : <'_>` "generic generic" type.
struct Foo<T_ : ?Sized + HKT>(
<Feed<'static, T_> as Trait>::Assoc,
)
where
Feed<'static, T_> : Trait,
;
With that, using Foo::<A>
fails, but using Foo::<B>
does not fail, even though they both amount to the same T = fn(&'static ())
.
- I think the distinction stems from
A
"capturing" 'static
, and thus being detected by the lifetime-eraser heuristic,
- whereas
B
does not capture anything on its own: it's just its associated type which does, but after being queried with a fed 'static
to the WithLifetime
trait. And somehow that lifetime in as WithLifetime<'static>
does not get erased