You've already provided the right answers to the issues at hand , except for an "explanation" to the encountered difference between async fn
and |...| async move { ... }
.
It turns out there are two outstanding bugs w.r.t. these higher-order signatures and bounds related to async closures:
-
Can't have a higher-order bound on an associated type
The first issue, is related to higher-order bounds in general, not necessarily related to closures. Basically, you can't introduce, within a function, an extra bound on a "higher-order assoc type" such as
for<'a> <T as Helper<'a, ...>>::Future : Unpin
, orfor<'a> <F as FnMut<(&'a mut A,)>>::Output : Future<...>
. I mean, you can, but no type will be able to meet the desired bounds, as showcased by the following example:trait Helper<'lt> { type Assoc; } fn foo<T> () where for<'any> T : Helper<'any, Assoc = ()> , // While the equality passes, the `Copy` doesn't!! for<'any> <T as Helper<'any>>::Assoc : Copy , {} impl Helper<'_> for () { type Assoc = (); // is indeed `Copy` } const _: () = { let _ = foo::<()>; // Error the trait `for<'any> Copy` is not // implemented for `<() as Helper<'any>>::Assoc` };
I think the issue is called / related to "lazy normalization", for those interested.
Note that this isn't the error that this thread has stumbled upon when using future-yielding-closures, since by using a helper trait, with a preemptive bound (
: Future... + Send
) on the associatedFut
type, this issue was dodged . -
Closures are almost never higher order, which affects future-returning closures.
The second issue is that you can't produce, with a closure, an anonymous future (e.g., using
async [move] { ... }
) that (re)borrows from the input parameters, at least not easily.To see why, consider this way simpler example:
let first = |xs: &'_ [i32]| -> Option<&'_ i32> { // <-----------------------+ xs.get(0) // | }; // | let elems = [42, 27]; // | dbg!(first(&elems)); // <- Infers the lifetime `'_` to be that of `elems` --+ let _: Option<&'static i32> = first(&[]); // Error
Basically, closures are very rarely higher-order: they, instead, use lifetime inference to be able to be called at least once in a way that works, which is why we rarely observe this limitation from closures.
Moreover, the only frequent situation where one could have hit this issue with closures is when feeding them to functions that do showcase higher-order closure bounds; but it turns out that for this very case, there is some kind of compiler-magical "back-pressure" from that closure bound which nudges the closure into becoming higher-order.
This is showcased by the funneling-into-higher-order-ness trick:
/// This is the *identity* function, but one which /// only takes higher-order closures as input, adding the constraint. /// Literally a funnel. fn higher_order_funnel<F> (f: F) -> F where F : FnOnce(&'_ [i32]) -> Option<&'_ i32>, { f } let first = higher_order_funnel(|xs: &'_ [i32]| -> Option<&'_ i32> { xs.get(0) }); let elems = [42, 27]; dbg!(first(&elems)); let _: Option<&'static i32> = first(&[]); // OK
With this, encountering a compiler-error caused by a non-higher-order closure, in the most common cases, is very rare; which is the reason these properties about closures are not that well-known.
The real issue is, this "back-pressured higher-order promotion" only happens with the closure traits (
Fn...
traits) verbatim: any kind of "equivalent subtrait" (e.g., the typical trait alias trick of subtrait + blanket impl) won't be able to "nudge the closure into higher-order-ness", such as with your: for<'a> Helper<'a, ...>
example.- Note that
async fn
s, on the other hand, are easily higher-order, which is why it works with them.
- Note that
So, to summarize:
-
If we use a concrete future type such as a
BoxFuture<'_, ...>
, the simple and directFn...
bounds suffice to make the callee's code work, and by using these directFn...
bounds, when feeding|...| async move { ... }.boxed()
closures, those get nudged into being higher order and everything Just Worksโข. -
In the other scenarios, we either hit the lazy normalization bug (on top of requiring
nightly
to be able to name (to bound it!) theOutput
associated type of theFn...
family) when directly using theFn...
traits and adding bounds on the return type, or we go through a helper proxy trait but then our closure doesn't get to be higher-order promoted.
That's the sad / painful situation with "async closures" as of today: boxing is currently required. Note that boxing already implicitly happens when using #[async_trait]
, which means most of the futures out there are already boxed anyways; and the code remains fast and performant nonetheless, since the size of those futures is, in practice, almost always very small, and thus these extra heap-allocations have thus a quite negligible cost
- I suspect it could be possible, with
min_type_alias_impl_trait
, and some macros, to be able to "name" the existential future type returned by the async closure, so as to be able to write ad-hoc funnelers and hopefully be able to define, with it, higher-order async closures... But it's a bit late for me, and I don't have that much free time, so I won't be testing this theory yet.