I need to select a TCP connection that is writable.
I wrote a seemingly innocent async function using futures buffer_unordered and to my surprise it compiled the first time. But when I actually tried to call it from underneath a tokio::spawn, the compiler didn't like it.
error: implementation of `FnOnce` is not general enough
--> src/lib.rs:305:9
|
305 | tokio::spawn(async move {
| ^^^^^^^^^^^^ implementation of `FnOnce` is not general enough
|
= note: closure with signature `fn(&'0 tokio::net::tcp::OwnedWriteHalf) -> impl futures::Future<Output = std::result::Result<(), std::io::Error>>` must implement `FnOnce<(&tokio::net::tcp::OwnedWriteHalf,)>`, for any lifetime `'0`...
= note: ...but it actually implements `FnOnce<(&tokio::net::tcp::OwnedWriteHalf,)>`
The error disappears when I remove .await.
Or it disappears if I remove buffer_unordered but call .await on the first item in the vector (I mean mirrors[0].writable().await), even though ti doesn't make much sense on an empty vector.
Finally, there was a tentative PR in the compiler to fix it, but which did not pan out; it does nonetheless has a lot of valuable (technical) information about why this issue occurs:
In your case, the lifetime-infected associated type comes from:
|s| s.writable()
this is a closure taking a &'mirrors OwnedHalf input (through inference), which has a FnOnce::Output associated type which is a Future with, itself, an Output associated type to resolve to the Result<()> that the async fn writable() resolves to.
I don't know which of those two layers of associated types (maybe both?) happen to be the offending one, but at least one of those two associated types is required to be Send, in order for the whole async move { … } block to be (required by tokio::spawn()).
And (at least one of) this associated type crosses an .await point, triggering the compiler bug, wherein it doesn't want to have a : Send bound on an associated type that involves a non-'static lifetime parameter (in this case, the 'mirrors borrow).
It turns out, that if the closure were changed to have a higher-order signature, like this:
then that fixed lifetime parameter infecting associated types would be no more, and so the compiler would be able to nicely resolve the required : Send bound.
Hence why it complained about the lack of such an impl.
Note that on more recent compiler, the errors become a bit more terse, pointing to the root cause (which is great!) but no longer pointing to the code that could be changed to tackle it (which is not great!).
It has gone from:
error: implementation of `FnOnce` is not general enough
--> src/main.rs:54:28
|
54 | let _: &dyn Send = &fut;
| ^^^^ implementation of `FnOnce` is not general enough
|
= note: closure with signature `fn(&'0 tokio::net::tcp::OwnedWriteHalf) -> impl futures::Future<Output = Result<(), std::io::Error>>` must implement `FnOnce<(&tokio::net::tcp::OwnedWriteHalf,)>`, for any lifetime `'0`...
= note: ...but it actually implements `FnOnce<(&tokio::net::tcp::OwnedWriteHalf,)>`
but this is almost never usable in practice (needs the exact right level of receiver indirection, and 0-captures whatsoever).
The more generally-applicable approach is to annotate the lifetime placeholder in the closure input type, instead of letting type inference figure it out:
I'm personally not super-pleased with these solutions because requiring higher-order closures is strictly more limiting than a fixed-lifetime one, so it means there will be cases where this workaround will break because of other constraints. But if it can get 90% of the cases unstuck, then that's at least something, right?