Skip to content

Add more helpers for Future #594

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
NobodyXu opened this issue May 29, 2025 · 12 comments
Open

Add more helpers for Future #594

NobodyXu opened this issue May 29, 2025 · 12 comments
Labels
api-change-proposal A proposal to add or alter unstable APIs in the standard libraries I-async-nominated T-libs-api

Comments

@NobodyXu
Copy link

NobodyXu commented May 29, 2025

Proposal

Problem statement

I propose to add more helpers to the std::future::Future trait to make it easier to use for async code.

Motivating examples or use cases

futures_util::FutureExt is widely used in async code as the stdlib does not provide the helper functions needed for many async crate.

std::iter::Iterator on the other hand, has lots of helper functions and thus most crates don't need to pull in any extra dependencies to use it, as compared to Future.

Solution sketch

I propose to add the following provided trait methods, similar to how Iterator works to enable simple usage and ability to override for better/more efficient implementation:

impl Future {
    /// Wrap the future in a Box, pinning it.
    fn boxed<'a>(self) -> Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>
    where
        Self: Sized + Send + 'a;

    /// Wrap the future in a Box, pinning it.
    ///
    /// Similar to `boxed`, but without the `Send` requirement.
    fn boxed_local<'a>(self) -> Pin<Box<dyn Future<Output = Self::Output> + 'a>>
    where
        Self: Sized + 'a;
}

And join for array/tuple

impl<T, const N: usize> [T; N]
where
    T: Future
{
    async fn join(self) -> [T::Output; N];
}

impl<T, O, R, const N: usize> [T; N]
where
    T: Future<Output = O>,
    O: Try<Residual = R>,
    R: Residual<[R::Output; N]>,
{
    async fn try_join<U>(self) -> <R as Residual<[R::Output; N]>>::TryType;
}

impl<T1, ...> (T1, ...)
where
    T1, ...: Future
{
    async fn join(self) -> (T1::Output, ...);
}

impl<T1, ..., O1, ..., R1, ..., const N: usize> (T1, ...)
where
    T1, ...: Future<Output = O>,
    O1, ...: Try<Residual = R>,
    R1, ...: Residual<(R1::Output, ...)>,
{
    async fn try_join<U>(self) -> <R as Residual<(R1::Output, ...)>>::TryType;
}
@NobodyXu NobodyXu added T-libs-api api-change-proposal A proposal to add or alter unstable APIs in the standard libraries labels May 29, 2025
@NobodyXu NobodyXu changed the title Add more helpers to std::future::Future trait Add more helpers to std::future::Future trait and add FusedFuture May 29, 2025
@NobodyXu NobodyXu changed the title Add more helpers to std::future::Future trait and add FusedFuture Add more helpers to std::future::Future trait and FusedFuture May 29, 2025
@taiki-e
Copy link
Member

taiki-e commented May 29, 2025

I would recommend reading past discussions on this (e.g., rust-lang/rust#111347). There have been several people who have suggested such a thing in the past, but none of them were accepted because of several unresolved questions.

@taiki-e
Copy link
Member

taiki-e commented May 29, 2025

Except for what already pointed out in the past discussion I linked above, the things I was wondering about are:

  • As for FusedFuture note that futures-core plans to remove it in the next breaking release. (See futures-core 1.0 release futures-rs#2207 (comment) for details.)
  • Also, you mention futures_util::FutureExt, but you also suggest adding APIs that futures_util::FutureExt does not have (Future::pinned) and has different signature (Future::now_or_never).
  • Also, it would be nice to have an explanation of the rationale for selecting these out of the many methods of futures_util::FutureExt.
    • For example, now_or_never is an API with pitfalls regarding cancellation safety.

@the8472
Copy link
Member

the8472 commented May 29, 2025

Note that with supertrait item shadowing v2 at least the name overloading should become less problematic.

@NobodyXu NobodyXu changed the title Add more helpers to std::future::Future trait and FusedFuture Add more helpers for Future May 29, 2025
@NobodyXu
Copy link
Author

NobodyXu commented May 29, 2025

Thanks, I've made some changes:

  • rm Future::map, Future::flatten and Future::then since using async/await is good enough, for Future::then it also introduces the complexity of async closure
  • replace Future::now_or_never with impl<T> From<Poll<T>> for Option<T>, as the latter is simpler without cancellation issue (could be a separate api-change?)
  • replace Future::pinned with Future::poll_unpinned, I previously put a pinned since I believed it was more versatile (can be used with now_or_never)

I think it might still be useful for manually wrapping multiple future, i.e. join in a specific order, with FusedFuture stdlib can avoid adding an extra Option if the future is already fused via specialization (IIRC stdlib can use unstable specialization for implementation details).

While most async {} automatically generated code is not fused, manually written futures often are fused, and it can be a nice property to expose.

  • Also, it would be nice to have an explanation of the rationale for selecting these out of the many methods of futures_util::FutureExt.
  • Future::boxed* is useful, as it avoid the annoying Box::pin(...) as Pin<Box<dyn Future + Send>> which could be long and tedious, and it can also specialise to avoid reboxing, if the future is already boxed
  • Future::poll_unpin is quite convenient as it is less tedious than Pin::new(&mut fut).poll(cx)
  • join/try_join for array/tuple is intuitive, and can be quite useful

I think other methods of FutureExt is not quite useful.

select! is something quite complicated and still evolving with its API.

std::async_iter has other select functions covered.

@programmerjake
Copy link
Member

there's a WIP language feature to allow calling Pin<&mut impl Unpin> methods with &mut directly, so poll_unpin may be deprecated soon.

@NobodyXu
Copy link
Author

there's a WIP language feature to allow calling Pin<&mut impl Unpin> methods with &mut directly, so poll_unpin may be deprecated soon.

Thanks, removed the function

@m-ou-se
Copy link
Member

m-ou-se commented Jun 3, 2025

cc @rust-lang/wg-async

@Darksonn
Copy link

Darksonn commented Jun 4, 2025

This really needs to be split up into several proposals.

FusedFuture

Tokio explicitly decided to go away from FusedFuture in its select! macro because it was believed to not be a good solution. Granted, Tokio's select! also has issues, but I don't think going back to FusedFuture is the answer.

impl<T> From<Poll<T>> for Option<T>;

Why?

join and try_join

Why use a method over a macro? I think that macro syntax is nicer to read:

let (res1, res2) = join!(fut1, fut2).await;

vs

let (res1, res2) = (fut1, fut2).join().await;

I mean maybe I could be convinced otherwise, but like the other proposals here, I would like to see alternatives mentioned, and I would like to see a reason for why one approach is chosen over the alternatives.

@taiki-e
Copy link
Member

taiki-e commented Jun 4, 2025

I think it might still be useful for manually wrapping multiple future, i.e. join in a specific order, with FusedFuture stdlib can avoid adding an extra Option if the future is already fused via specialization (IIRC stdlib can use unstable specialization for implementation details).

While most async {} automatically generated code is not fused, manually written futures often are fused, and it can be a nice property to expose.

The problem is, as I said in the linked comment, that it cannot be correctly implemented without specializations. (Using specialization in std is not enough to do things correctly.)

Even if the std/compiler can optimize it, it will only leave something broken unless the implementation on which the optimization depends is correctly implemented.

@NobodyXu
Copy link
Author

NobodyXu commented Jun 5, 2025

Why use a method over a macro? I think that macro syntax is nicer to read:

Suppose you have a function taking an array of arbitrary, using join! simply won't work as you don't know the length of the array:

fn f<Fut: Future<Output = ()>, const N: usize>(futures: [Fut; N]) {
    join!(/* ? */);
}

And using join! adds a bunch of code to the caller in an opaque and hard to audit/understand way (macro like pin! could even add local variables), where as a function has a clear boundary.

Having a join methods for tuple would be ergonomic, if rust ever has varadic tuple that can be passed to generic functions, compared to macro that inserts code into the caller:

[fut1, fut2].join().await;
(fut1, fut2).join().await;

The problem I just realized though is [T]::join conflicts with [T; N]::join, so maybe a different name is required.

Even if the std/compiler can optimize it, it will only leave something broken unless the implementation on which the optimization depends is correctly implemented.

Thanks, so it'd require unsafe for it to even work...in that case it indeed isn't very useful, I've removed it

impl<T> From<Poll<T>> for Option<T>;

I think Poll actually matches Option quite well: Either you have a return value or you don't, that'd be useful for cases where future is only poll once (FutureExt::now_or_never).

@taiki-e
Copy link
Member

taiki-e commented Jun 5, 2025

impl<T> From<Poll<T>> for Option<T>;

I think Poll actually matches Option quite well: Either you have a return value or you don't, that'd be useful for cases where future is only poll once (FutureExt::now_or_never).

If we adding a helper for such case, I think it is preferable that this be an explicit method rather than a From implementation.

But I'm not sure that is really necessary, and also note that now_or_never uses noop_context so it does not work just using it.

@NobodyXu
Copy link
Author

NobodyXu commented Jun 5, 2025

But I'm not sure that is really necessary, and also note that now_or_never uses noop_context so it does not work just using it.

That's true, I've removed the From impl from proposal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-change-proposal A proposal to add or alter unstable APIs in the standard libraries I-async-nominated T-libs-api
Projects
None yet
Development

No branches or pull requests

7 participants