Skip to content

try{}: What does homogeneity mean for Poll? #155368

Open

Description

@scottmcm

The idea of rust-lang/rfcs#3721 is that you get "the same" thing out of try { x? }.

However, there's currently no requirement that that actually happen in either the trait nor the desugaring.

As usual, Poll is the problem child for this: https://doc.rust-lang.org/std/task/enum.Poll.html#impl-Try-for-Poll%3COption%3CResult%3CT,+E%3E%3E%3E

Some potential options:

  1. Add a bound like type Residual: Residual<Self::Output, TryType = Self>; to the Try trait so that this can't happen any more.
    • that insists on separately-coloured residuals. This would require changing the residual types for Poll to be distinct, and to update the FromResidual implementations accordingly to continue to support stable things.
  2. Give up and say "meh, whatever" and try { my_poll? } will give a Result or an Option.
  3. Add a bound into the desugaring for ? or try { somehow that would enforce that it's usable only on Try types with full homogeneity
    • perhaps merge the branch+from_residual into one helper function that calls both but which has some extra where bounds

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Apr 16, 2026
  2. added
    T-langRelevant to the language team
    T-libs-api[DEPRECATED; DO NOT USE]
    F-try_blocks`#![feature(try_blocks)]`
    on Apr 16, 2026
  3. MusicalNinjaDad commented on Apr 26, 2026

    @MusicalNinjaDad
    Contributor

    Coming at this from the Try perspective

    I would expect my_poll? to be the equivalent of ready!(mypoll) - thus allowing the ready! macro to go the same way that the try! macro has.

    That would mean we had (possible typos as I'm doing this outside an IDE and deliberately spelling out the assoc types):

    impl Try for Poll<T> {
        Output = T,
        Residual = Poll<!>
    
        fn branch (self) -> ControlFlow<Poll<!>, T> {
            self::Ready(t) => ControlFlow::Continue(t),
            self::Pending => ControlFlow::Break(Poll::Pending),
        }
    
        fn from_output(output: T) -> Self {
            Self::Ready(output)
        }
    }
    
    impl FromResidual<Poll<!>> for Poll<T> {
        fn from_residual(residual: Poll<!>) -> Self {
            match residual {
                Poll:Pending => Self::Pending,
            }
        }
    }

    Which would, of course conflict with the two existing specific impls. Thankfully they are both still nightly-only otherwise that would be impossibly breaking for users.

    Looking at where I've used the current impl

    it does, sort of make sense ...

    /// The future returned by [UdpStream::next]
    #[derive(Debug)]
    pub struct Next<'stream, const BUF_SIZE: usize> {
        stream: &'stream mut UdpStream<BUF_SIZE>,
    }
    
    impl<'stream, const BUF_SIZE: usize> Future for Next<'stream, BUF_SIZE> {
        type Output = Option<io::Result<([u8; BUF_SIZE], usize, SocketAddr)>>;
    
        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
            let this = &mut *self;
            let stream = &mut *this.stream;
            ready!(stream.poll_read_ready_unpin(cx)?); // <- ? on a Poll<Result<Self::Ok, Self::Err>> inside a ready!() expansion

    calling ready!(poll_wrapping_result_T?) works inside a function returning Poll<Result<T>>

    How could we get same usability and be consistent with Try and the rest of the std lib?

    Option & Result both offer a transpose method as well as Result::ok()/Result::err()/Option::ok_or() for handling similar situations.

    If we were ok (!) with Poll's assumption that people will always want to pull out the Result in a Poll<Option<Result<T>>> then it would be easy enough to add a similar Poll<Result<T,E>>::transpose(self) -> Result<Poll<T>,E> along with a Result<Poll<T>,E>::ok_or_pending(self) -> Poll<T> (etc...)

    That would allow the example above to be explicit:

    /// The future returned by [UdpStream::next]
    #[derive(Debug)]
    pub struct Next<'stream, const BUF_SIZE: usize> {
        stream: &'stream mut UdpStream<BUF_SIZE>,
    }
    
    impl<'stream, const BUF_SIZE: usize> Future for Next<'stream, BUF_SIZE> {
        type Output = Option<io::Result<([u8; BUF_SIZE], usize, SocketAddr)>>;
    
        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
            let this = &mut *self;
            let stream = &mut *this.stream;
            stream.poll_read_ready_unpin(cx).transpose().ok_or_pending()?;

    As a user I find this much better as I can also handle cases where I want to map a Poll<Option<Result::Err>> to Poll::Ready<None> as well as cases where I want to map to Poll::Pending

  4. MusicalNinjaDad commented on Apr 26, 2026

    @MusicalNinjaDad
    Contributor

    To answer the original question:

    1. say "meh, whatever" and some types will implement non-homogeneous Try, but
      • recommend against it in the docs (unless there is a VERY good reason to do it)
      • set a good example with the standard lib by fixing Poll (and even consider Poll's current implementation blocking to the stabilisation of Try)
  5. MusicalNinjaDad commented on Jun 9, 2026

    @MusicalNinjaDad
    Contributor

    for full homogeneity would it be valuable to also extend trait Residual to require:

    pub const trait Residual<O>: Sized {
        /// The "return" type of this meta-function.
        type TryType: [const] Try<Output = O, Residual = Self> + FromResidual<Self>;
                                                                 ^^^^^^^^^^^^^^^^^^

    sadly, this would not (always) reduce the need to add a bound in generic code but it certainly would help with ensuring consistent implementations

  6. added
    T-libsRelevant to the library team, which will review and decide on the PR/issue.
    and removed
    T-libs-api[DEPRECATED; DO NOT USE]
    on Aug 12, 2026
  7. camsteffen commented on Oct 8, 2026

    @camsteffen
    Contributor

    I wonder if we should have a lint that fires on Try impls where the following occurs:

    <<Self as Try>::Residual as Residual>::TryType != Self
    

    In other words enforce that Try + Residual should "round-trip" a Try type back to itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    F-try_blocks`#![feature(try_blocks)]`F-try_trait_v2Tracking issue for RFC#3058T-langRelevant to the language teamT-libsRelevant to the library team, which will review and decide on the PR/issue.needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions