impl<T: Future> Future for Option<T>

Yes, technically adding trait impl to the existing type doesn't requires the RFC process and get insta-stabilized. But I think what I want to do is not very obvious and maybe too opinionated so I wanted to collect more thoughts before sending the PR.

IMO Option<T: Future> should impl Future<Output = Option<T::Output>>. I guess most people wouldn't disagree with it. The possibly-opinionated part is, that I think Option<impl Future> should be fused - polling it should keep returning Ready(None) after the first Ready result. Implementation would be fairly trivial thanks to the Pin::set() function.

Does it seems too opinionated for the stdlib? Or am I overthinking it? Please let me know how do you think about it.

I don't think it's a good idea to implement Future for Option directly, since its purpose is not to be a future - how about implementing IntoFuture instead? Then it will still be usable with .await syntax, but I think the intention is a bit clearer.

I am a bit doubtful about the real cases in which you have Option<T: Future>. My point is that if you have an async fn, you will necessarily get an impl Future<Option<T>>, so we are only talking about normal functions (therefore not usable in async contexts) that return an optional Future.

To date I see a similar behavior whenever you need to create a Stream, because we cannot express this particular async behaviour without creating a concrete stream type (we would need something like yield await). On the other hand, I never found myself in the situation of retuning an Option<T: Stream>, but surely I returned something like Result<impl Stream<Item = Result<T, E>>, E>.

Given that I would prefer impl IntoFuture for Option like @Kestrer already said, I would like to know if there are any crates that rely on returning Option<T: Future>, just to better understand the specific use case.

Bonus question: what about Result? It should be fine having IntoFuture for Result<T: Future, E> -> impl Future<Item = Result<T::Output, E>>, right?

If we impl<T> IntoFuture for Option<T> where T: Future, I feel like we should also have a similar thing for functions, like impl<T, R, ..A> Fn(..A) -> Option<R> for Option<T> where T: Fn(..A) -> R. We should also add impl<T, R> Add<R> for Option<T> where T: Add<R> wherein type Output = Option<R>;. Except, the current precedence is we don't, so I don't feel like we should add the IntoFuture implementation for Option, (at least not without also adding other convenience implementations for Option, and maybe Result).

In a "perfect world," you could option.map(|f| f.await) and it'd do "the obvious thing". In real life Rust, it'd probably require postfix macros (e.g. option.map!(f => f.await)) or a special map_async method.

I think we can justify IntoFuture for Option the same way that we justified IntoIterator for Option. Maybe that extends to Result as well. But on the other hand, clippy has a deny-by-default lint for using these IntoIterator implementations (via for loop), so it's not unlikely that clippy'd grow a lint that does the same thing for .await on Option/Result.

2 Likes

Clippy would warn on Some(async { xyz }).await just like it warns on for _ in Some(xyz), but it wouldn't warn on Some(xyz).map(async_function).await the same way it doesn't warn on for _ in Some(xyz).into_iter().flat_map(something) (I don't think it does at least).

But I think we should be wary of trying to draw similarities between IntoIterator and IntoFuture - the two implementations would be very different. In functional terminology, IntoIterator for Option simply transforms a monad to an equivalent monad, whereas IntoFuture transposes a nested monad (Option<Future> -> Future<Option>).