rust-lang / rust

Empowering everyone to build reliable and efficient software.

Home Page:https://www.rust-lang.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Tracking Issue for `try_trait_v2`, A new design for the `?` desugaring (RFC#3058)

scottmcm opened this issue · comments

This is a tracking issue for the RFC "try_trait_v2: A new design for the ? desugaring" (rust-lang/rfcs#3058).
The feature gate for the issue is #![feature(try_trait_v2)].

This obviates rust-lang/rfcs#1859, tracked in #42327.

About tracking issues

Tracking issues are used to record the overall progress of implementation.
They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions.
A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature.
Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.

Steps

  • Implement the RFC
    • Add the new traits and impls
    • Update the desugar in ast lowering
    • Fixup all the tests
    • Add nice error messages in inference
    • Improve perf with enough MIR optimizations
    • Delete the old way after a bootstrap update #88223
  • Not strictly needed, but a mir-opt to simplify the matches would really help: #85133
  • Add more detailed documentation about how to implement and use the traits
  • Decide whether to block return types that are FromResidual but not Try
  • Fix rustdoc to show the default type parameter on FromResidual better (Issue #85454)
  • Before stabilizing, ensure that all uses of Infallible are either fine that way or have been replaced by !
  • Stabilizing this will allow people to implement Iterator::try_fold
    • As part of stabilizing, document implementing try_fold for iterators (perhaps reopen #62606)
    • Ensure that the default implementations of other things have the desired long-term DAG, since changing them is essentially impossible later. (Specifically, it would be nice to have fold be implemented in terms of try_fold, so that both don't need to be overridden.)
  • Adjust documentation (see instructions on rustc-dev-guide)
  • Stabilization PR (see instructions on rustc-dev-guide)

Unresolved Questions

From RFC:

  • What vocabulary should Try use in the associated types/traits? Output+residual, continue+break, or something else entirely?
  • Is it ok for the two traits to be tied together closely, as outlined here, or should they be split up further to allow types that can be only-created or only-destructured?

From experience in nightly:

  • Should there be a trait requirement on residuals of any kind? It's currently possible to accidentally be FromResidual from a type that's never actually produced as a residual (rwf2/Rocket#1645). But that would add more friction for cases not using the Foo<!> pattern, so may not be worth it.
    • Given the trait in #91286, that might look like changing the associated type Residual; to type Residual: Residual<Self::Output>;.

Implementation history

We have a problem in our project related to the new question mark desugaring. We use the track_caller feature in From::from implementation of the error types to collect stack traces with generics and auto and negative impl traits magic implemented by @sergeyboyko0791 (https://github.com/KomodoPlatform/atomicDEX-API/blob/mm2.1/mm2src/common/mm_error/mm_error.rs).

After updating to the latest nightly toolchain this stack trace collection started to work differently. I've created a small project for the demo: https://github.com/artemii235/questionmark_track_caller_try_trait_v2

cargo +nightly-2021-05-17 run outputs Location { file: "src/main.rs", line: 18, col: 23 } as we expect.

cargo +nightly-2021-07-18 run outputs Location { file: "/rustc/c7331d65bdbab1187f5a9b8f5b918248678ebdb9/library/core/src/result.rs", line: 1897, col: 27 } - the from_residual implementation that is now used for ? desugaring.

Is there a way to make the track caller work the same way as it was before? Maybe we can use some workaround in our code?

Thanks in advance for any help!

That's interesting -- maybe Result::from_residual could also have #[track_caller]? But that may bloat a lot of callers in cases that won't ever use the data.

From the description:

A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature.

@artemii235 Do you mind opening a separate issue?

Do you mind opening a separate issue?

No objections at all 🙂 I've just created it #87401.

May i suggest changing branch method's name to something else? When searching for methods, it's a little not obvious to see Option::branch or Result::branch is not the method one should usually call...

How do I use ? with Option -> Result now? Before it was only necessary to implement From<NoneError> for my error type.

Use .ok_or(MyError)?

Why the implementation of FromResidual for Result uses trait From in stead of Into. According to the documentation of trait Into and From, we should

Prefer using Into over From when specifying trait bounds on a generic function to ensure that types that only implement Into can be used as well.

Clarification is welcome as an error type implementing only Into trait arises with associated error type on traits and associated types cannot bind on From for lack of GATs.

@RagibHasin

see #31436 (comment)
and #31436 (comment)
(and the following discusion, respectively)

Hi, I'm keen to see this stabilized. Is there any work that can be contributed to push this forward? It would be my first Rust contribution, but I have a little experience working on compiler code (little bit of LLVM and KLEE in college).

@BGR360 Unfortunately the main blockers here are unknowns, not concrete-work-needing-to-be-done, so it's difficult to push forward. It's hard to ever confirm for sure that people don't need the trait split into parts, for example.

Have you perhaps been trying it out on nightly? It's be great to get experience reports -- good or bad -- about how things went. (For example, #42327 (comment) was a big help in moving to this design from the previous one.) If it was good, how did you use it? If it was bad, what went wrong? In either case, was there anything it kept you from doing which you would have liked to, even if you didn't need it?

Experience Report

@scottmcm I have tried #[feature(try_trait_v2)] in its current form. I'll give an experience report:

Overall my experience with this feature is positive. It may end up being critical for my professional work in Rust.

My use case is very similar to @artemii235: #84277 (comment). At my work, we need a way to capture the sequence of code locations that an error propagates through after it is created. We aren't able to simply use std::backtrace to capture the backtrace at the time of creation, because errors can propagate between multiple threads as they bubble up to their final consumer. The way we do this in our C code is to manually wrap every returned error value in a special forward_error macro which appends the current __file__, __line__, and __func__ to the error's backtrace.

We would love to be able to do this in our Rust code using just the ? operator, no macros or boilerplate required. So I experimented with implementing my own replacement for std::result::Result (call it MyResult). I implemented std::ops::Try on MyResult in a very similar manner to std::result::Result, but I annotated FromResidual::from_residual with #[track_caller] so that I could append the location of the ? invocation to the error's backtrace. The experiment was successful and relatively straightforward.

To get this to work, I made express use of the fact that you can implement multiple different FromResidual on a type (I think that might be what you're referring to when you say "splitting the trait into parts"?). I have one FromResidual to coerce from std::result::Result to my MyResult, and another one to coerce from MyResult to MyResult.

I'd be happy to give more specifics on how I achieved my use case either here or on Zulip, just let me know :)

Pros:

  • Allows me to implement multiple FromResidual for my Try type. This was critical for my use case.

Cons:

  • Documentation is a little weak, but I was able to learn by example by reading the source code for std::result::Result.
  • It'd be great to be able to achieve my use case without having to rewrite Result. See my other comment below.

Experience report

I was using try_trait on an app of mine and upgraded to try_trait_v2 because the build started failing on the latest nightly. My use case was a bit weird as I am using the same type of Ok and Err variants as it is a generic Value type for a programming language. However the try operator is still incredibly helpful in the implementation.

Pros:

  • The conversion was localized.

Cons:

  • More code to get it to work.
  • Many more new concepts than try_trait. For example I now need to use:
    • ControlFlow which is fairly straight forward (although I don't know why the arguments are backwards compared to Result.
    • Residual which I still barely understand and the name is incredibly perplexing. "Residue" is something left over but it isn't clear what is being left over in this case.
  • The docs are not very helpful. I had to guess the impl<E: Into<Val>> std::ops::FromResidual<Result<std::convert::Infallible, E>> for Val incantation from the error messages and it still isn't completely clear to me how this type comes to be.

Overall this v2 is a clear downgrade for this particular use case however the end result isn't too bad. If this is making other use cases possible it is likely worth it with better names and docs.

The full change: https://gitlab.com/kevincox/ecl/-/commit/a1f348633afd2c8dd269f95820f95f008b461c9e

So I experimented with implementing my own replacement for std::result::Result (call it MyResult).

This is actually a little bit unfortunate, in retrospect. It would be much better if I could just make use of std::result::Result as it already exists. That would require two things that are missing:

  • <std::result::Result as FromResidual>::from_residual would need to have #[track_caller]
  • I would need to be able to intercept invocations of From<T>::from() -> T so I can push to the stack even when the ? operator does not coerce the result to a different error type.

To illustrate, here's how things work in my experiment:

pub struct ErrorStack<E> {
    stack: ..,
    inner: E,
}

impl<E> ErrorStack<E> {
    /// Construst new ErrorStack with the caller location on top.
    #[track_caller]
    fn new(e: E) -> Self { ... }

    /// Push location of caller to self.stack
    #[track_caller]
    fn push_caller(&mut self) { ... }

    /// Return a new ErrorStack with the wrapped error converted to F
    fn convert_inner<F: From<E>>(f: F) -> ErrorStack<F> { ... }
}

pub enum MyResult<T, E> {
    Ok(T),
    Err(ErrorStack<E>),
}

pub use MyResult::Ok;
pub use MyResult::Err;

impl<T, E> Try for MyResult<T, E> {
    type Output = T;
    type Residual = MyResult<Infallible, E>;

    /* equivalent to std::result::Result's Try impl */
}

/// Pushes an entry to the stack when one [`MyResult`] is coerced to another using the `?` operator.
impl<T, E, F: From<E>> FromResidual<MyResult<Infallible, E>> for MyResult<T, F> {
    #[inline]
    #[track_caller]
    fn from_residual(residual: MyResult<Infallible, E>) -> Self {
        match residual {
            // seems like this match arm shouldn't be needed, but idk the compiler complained
            Ok(_) => unreachable!(),
            Err(mut e) => {
                e.push_caller();
                Err(e.convert_inner())
            }
        }
    }
}

/// Starts a new stack when a [`std::result::Result`] is coerced to a [`Result`] using `?`.
impl<T, E> FromResidual<std::result::Result<Infallible, E>> for Result<T, E> {
    #[inline]
    #[track_caller]
    fn from_residual(residual: std::result::Result<Infallible, E>) -> Self {
        match residual {
            // seems like this match arm shouldn't be needed, but idk the compiler complained
            std::result::Result::Ok(_) => unreachable!(),
            std::result::Result::Err(e) => Err(StackError::new(e)),
        }
    }
}

If std::result::Result had #[track_caller] on its FromResidual::from_residual, then I could avoid everything above by just pushing to the stack inside an impl From:

impl<E, F: From<E>> From<ErrorStack<E>> for ErrorStack<F> {
    #[track_caller]
    fn from(mut e: ErrorStack<E>) -> Self {
        e.push_caller();
        e.convert_inner()
    }
}

However, this does not work because it conflicts with the blanket From<T> for T implementation.

I could limit my From to types E, F such that E != F, but I need functions to show up in my error trace even if the residual from ? does not change types. For example:

fn foo() -> MyResult<(), io::Error> {
    fs::File::open("foo.txt")?;
}

fn bar() -> MyResult<(), io::Error> {
    // I need bar to show up in error traces, so I wrap with Ok(..?).
    // Without my custom MyResult, I am unable to intercept this invocation of the `?` operator, because
    // the return type is the same as that of `foo`.
    Ok(foo()?)
}

Why the Option<Infallible> is the Option's Residual type?
Why not the option itself: Option<T> ?

This would let me do:

impl FromResidual<Option<Viewport>> for MyResult {
    fn from_residual(_: Option<Viewport>) -> Self {
        Self(Err(SomeError::ViewportNotFound))
    }
}
impl FromResidual<Option<Item>> for MyResult {
    fn from_residual(_: Option<Item>) -> Self {
        Self(Err(SomeError::ItemNotFound))
    }
}

This ok_or(Error)? is bugging me, and I really want a solution that converts Option<T> to MyResult.
If I would convert Option<Infallible> to a MyNoneError it wouldn't help me at all. Even .expect() would add more info about the place.

For workaround, I created a module that contains the error handling like:

pub fn viewport<'p>(
    viewports: &'p HashMap<ViewportId, Viewport>,
    viewport_id: &ViewportId,
) -> Result<&'p Viewport, BindingError> {
    viewports
        .get(viewport_id)
        .ok_or(BindingError::ViewportDoesNotExist)
}
pub fn viewport_mut<'p, 'msg>(
    viewports: &'p mut HashMap<ViewportId, Viewport>,
    viewport_id: &ViewportId,
) -> Result<&'p mut Viewport, BindingError> {
    viewports
        .get_mut(viewport_id)
        .ok_or(BindingError::ViewportDoesNotExist)
}
...

It looks really bad. But the usage is only 1 line compared to 3.

So will this be improved?

Why the Option<Infallible> is the Option's Residual type?

Because from_residual should always be called with None. Option<!> would also work, if ! is stabilized before the Try trait. ! or Infallible communicates that through the type system.

If/once RFC-1210 is stabilized, then I think the Option's Try trait could be implemented like:

impl<T> ops::Try for Option<T> {
    type Output = T;
    default type Residual = Option<convert::Infallible>;

    #[inline]
    fn from_output(output: Self::Output) -> Self {
        Some(output)
    }

    #[inline]
    default fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
        match self {
            Some(v) => ControlFlow::Continue(v),
            None => ControlFlow::Break(None),
        }
    }
}

And you could change the Residual type for your specific Option type.

Meanwhile, I found an even better workaround. I put it here. This may be useful for somebody else too:

  1. DefineSomeError and MyResult. MyResult is needed because I'm not allowed to impl the std's Result, so I applied the new type pattern.
  2. Implement From<FromResidual<PhantomData<T>>> for MyResult (where T is from the Option<T> that you want to use.)
#[derive(Debug)]
pub enum SomeError {
    NoStringError,
    NoIntError,
}
struct MyResult(Result<i32, SomeError>);

// allow convert Option<T> i
impl FromResidual<PhantomData<String>> for MyResult {
    fn from_residual(_: PhantomData<String>) -> Self {
        Self(Err(SomeError::NoStringError))
    }
}
impl FromResidual<PhantomData<i32>> for MyResult {
    fn from_residual(_: PhantomData<i32>) -> Self {
        Self(Err(SomeError::NoIntError))
    }
}
  1. Define MyOption (An Option<T> wrapper for the same reasons as for MyResult).
  2. impl From<Option<T>> to let rust convert any option to this MyOption.
  3. Implement FromResidual and Try for this MyOption
struct MyOption<T>(Result<T, PhantomData<T>>);

// let any Option<T> be MyOption<T>
impl<T> From<Option<T>> for MyOption<T> {
    fn from(option: Option<T>) -> Self {
        match option {
            Some(val) => Self(Ok(val)),
            None => Self(Err(PhantomData)),
        }
    }
}

// Allow '?' operator for MyOption
impl<T> FromResidual<PhantomData<T>> for MyOption<T> {
    fn from_residual(o: PhantomData<T>) -> Self {
        Self(Err(o))
    }
}
impl<T> Try for MyOption<T> {
    type Output = T;
    type Residual = PhantomData<T>;
    fn from_output(output: Self::Output) -> Self {
        MyOption(Ok(output))
    }
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
        match self.0 {
            Ok(val) => ControlFlow::Continue(val),
            Err(err) => ControlFlow::Break(err),
        }
    }
}
  1. define a macro for convenience.
macro_rules! e {
    ($($token:tt)+) => {
        MyOption::<_>::from($($token)+)?
    };
}

And now I can use this e! macro for any Option in a method that returns MyResult.

fn get_some_string() -> Option<String> { Some(String::from("foo")) }
fn get_some_int() -> Option<i32> { Some(42) }

fn foo() -> MyResult {
    let some_string = e!(get_some_string());
    let some_int = e!(get_some_int());

    MyResult(Ok(42))
}

@fxdave IMO not a great idea to ditch the whole Result API -- try this or newtype that hashmap or something. But you're onto something with this and eventually when you turned it into PhantomData<T>:

Why the Option<Infallible> is the Option's Residual type?
Why not the option itself: Option<T>?

The problem: you want to convert Option::<T>::None to Err for specific error types using only ?. I think this is pretty common. To be clear I think it has somewhat limited use, I wouldn't implement it unless I were sure absence was always an error for that type, lest a single character ? be the cause of mistakes. Constraining the implicit conversion to specific error types you only use when this statement holds is a good idea, like InViewportContextError.

The current Option::Residual is indeed annoying in that it erases the type it could have held, so that information can't be used for any conversions like the one you want. As I understand it the whole point of FromResidual is that it's where you glue together your own Try types with other people's.

Re @tmccombs' solution, I don't think making people implement a specialised Try for Option with a custom Residual is ideal. The specialised Try isn't even enough -- you'd need to implement FromResidual<MyResidual> on both Option<T> and Result<T, MyError> generically as well. Can those even be done outside std? I don't think it can for Option<T>. Maybe you'd just have Residual = Result<!, MyError>. I don't know. But it sounds way too much effort and a steep learning curve for a common thing.

Given this is kinda common, why not bring back good old NoneError? But this time, carry information about what type produced it. And given the v2 RFC is all about removing references to "errors", give it a new name accordingly.

// std
struct Absent<T>(PhantomData<T>);
impl Try for Option<T> {
    type Residual = Absent<T>;
    type Output = T;
    ...
}
impl<A, T> FromResidual<Absent<A>> for Option<T> { ... }
impl<A, T, E> FromResidual<Absent<A>> for Result<T, E> where E: From<Absent<A>> { ... }

// userland
struct Foo;
enum MyError { MissingFoo, MissingOtherType }
impl From<Absent<Foo>> for MyError { ... }
impl From<Absent<OtherType>> for MyError { ... }

fn get_foo() -> Option<Foo> { ... }
fn bar() -> Result<i32, MyError> {
    let foo = get_foo()?;
    Ok(42)
}

This is basically your workaround but in std where it should be. This isn't possible with Residual = Option<!> because the the T is erased and unavailable in Result::from_residual.

Benefits:

  • No need to wait for impl specialization to land.
  • Exactly the same API as people use now to make error types composable via ?.
  • When you write this in a codebase, it pretty directly communicates the idea that absence of the type is an error. Full-on specialised Try/FromResidual impls don't.

Problems:

  • Absent<&'_ T>and similarly &mut are a bit annoying. You can't add a second impl i.e. impl FromResidual<Absent<&'_ A>> for result wherever E: From<Absent<A>>, because E could implement From<Absent<&'_ A>> as well. So as it stands people would have implement From<Absent<&'a Foo>> on their error types to make .as_ref()? work.

I understand the RFC is also trying to avoid having to create residual types, because implementing Try on large enums was previously really annoying. That doesn't mean std has to use !/Infallible everywhere. There is nothing preventing std from using a neat little residual type to make life with Option and Result easier.

Try it: old, edit: more complete

One addition for completeness is that if the Enum variant types RFC ever comes out of postponement hibernation, it might cover some (not all) of these residual types-with-holes-in-them problems. Thinking about this also surfaced a usability problem that might have gone unnoticed due to the rustc features enabled in libcore so far.

  1. It is not specifically contemplated by that RFC, but if you could impl FromResidual<Option<T>::None> for MyTryType (noting that's different from impl Trait for Option<T>::None that it does contemplate forbidding) then that would be a much more easily understood way to define your residual types.

  2. With enum variant types, the Absent<T> idea could be replaced by Option<T>::None. Deprecate Absent<T> (a struct with no public fields) and alias it to the variant, and everything would still work. It would be very easy to do this kind of thing in your own code, too. So if you're worried about usability of the residual pattern for user-defined types, there's at least something on the distant horizon to ameliorate that.

  3. Then consider Result<T, E>::Err. This one is more of a worry. First, note that the try_trait_v2 RFC's example implementation (and the real one in libcore) of FromResidual<Result<!, E>> does not compile outside libcore with its many rustc features activated. On stable Rust, you have to do this: (playground)

let r: Result<core::convert::Infallible, i32> = Err(5);
match r {
    Err(five) => ...,
    // rustc demands an Ok match arm, even though it only contains an Infallible.
    // you must add:
    Ok(never) => match never {},
}

This also happens with #![feature(never_type)] and Result<!, i32>. So as it stands now using Result<Infallible, E>, the main use case for try_trait_v2, namely adding FromResidual implementations for custom types that interoperate with Result APIs, requires this weird workaround for infallible types. It's not as clean as it has been made out.

But also, if you ever simply swapped out Result<!, E> for Result<T, E>::Err, you'd mess up everyone's match residual { Ok(never) => match never {}, ... } arms, since they wouldn't compile with the variant type.

  1. You could do an Absent<T>-style solution for Result, by defining struct ResultErr<T, E>(PhantomData<T>, E) and only providing a single fn into_err(self) -> E method, so that nobody is relying on the infallible match arm behaviour. (No name is going to be as good as Absent 😞). That would also eliminate the usability problem with infallible matches identified above. It would require choosing an API that will eventually be present on Result<T, E>::Err, i.e. match up with rust-lang/rfcs#1723 or something.

In summary, if you stabilise the impl with type Residual = Result<!, E> there's no going back, everyone's going to have to wrap their head around the use of the infallible/never type in there forever. As I said in my last post, while it's nice that the pattern can be used to create ad hoc residual types for a decent class of enums with the current compiler, std doesn't have to use !. I would consider not using the pattern for Result either, rather using a dedicated type as above.

Also, if std contained no implementations of Try with a ! in the associated residual, it would become even more difficult to explain why it's called the residue / the residual.

I would suggest naming it Failure. We don't need to describe it in terms of abstract splits between outputs and anti-outputs, the Try trait is named Try and the operator is a question mark. If ? returns from the function, the answer is that we tried but did not succeed. When you ask yourself, "if you try a Result, what constitutes failure?" you must admit the answer is Err(E). You would not additionally rename Output to Success, because "what constitutes success" is Ok(T), not T.

pub trait Try: FromFailure<Self::Failure> {
    type Output;
    /// A type representing a short-circuit produced by this specific `Try` implementation.
    ///
    /// Each `Try` implementation should have its own `Failure` type, so that a custom
    /// conversion can be defined for every combination of the type `?` is used on
    /// (implementing `Try<Failure = F>`), and the return type of the function it is used
    /// within (implementing `FromFailure<F>`).
    ///
    /// (Docs can give an example of using ! if they like)
    type Failure;
    fn from_output(x: Self::Output) -> Self;
    fn branch(self) -> ControlFlow<Self::Failure, Self::Output>;
}

pub trait FromFailure<F = <Self as Try>::Failure> {
    /// Construct Self from a failure type, 
    fn from_failure(failure: F) -> Self;
}

This might have been discussed /dismissed somewhere already, but I don't really see any downsides. You've already got the perfectly abstract ControlFlow in there, no need to pretend that Try isn't about success/failure.

@cormacrelf as far as I remember, counter-points were e.g.:
#42327 (comment)
#42327 (comment)

e.g. in some cases we want to short-circuit on success or short-circuit in both success and error conditions, and the ControlFlow terminology matches this more closely than some Failure/Success distinction; this is also afaik basically the underlying motivation to do this trait-juggling at all, because otherwise we could just continue to use Result and Option, which would suffice in that case, but unfortunately, doesn't adequately cover other cases that should be covered.

Another possibility which might be interesting, would be replacing all of this just with ControlFlow as the primary building block, and defining adequate conversions for Result and Option from/into that. Another alternative might be some kind of PhTaggedControlFlow, e.g.

pub struct PhTaggedControlFlow<Tag, B, C = ()> {
    tag: PhantomData<Tag>,
    inner: ControlFlow<B, C>,
}

with appropriate conversions (including conversion into ControlFlow). This would have the downside that functionality to decide whether to Break or Continue would be more ad-hoc (although it could be wrapped mostly properly). Another disadvantage of that would be that it might be easier to accidentially end up with some ControlFlow->Result conversion which we overall would want to avoid (hence some tagged ControlFlow, to have more control about potential conversions, especially if we also need to deal with Results (and similar types) from other functions, and might want to handle them different for every such case, this would be more some kind of "stop-gap" thing to avoid some unintended "pollution by conversion possiblities", which might make code more unreadable (or nudge the user into sprinkling of .into() or such, which could quickly lead to fragile code, in case any of the possible conversions break)). I don't know exactly how justified that concern might be.

What about implement From trait when the two generic of ControlFlow are identical as impl From<ControlFlow<T, T>> for T. Would allow to use into() instead of doing a match "at hand".

Inspired by #45222 (comment)

I wonder if it would be feasible to have an optimization where for in loops are replaced with .try_for_each call. Currently it's not possible to implement this method manually, and the standard library implementations are well behaved, so this wouldn't be a breaking change. This couldn't be done after Try trait gets stabilized.

Of course, I guess one issue with that is .await within for in loop.

@xfix the compiler could easily scan for awaits in the code, tho, and decide based on that, I don't think it would be a big problem.

Await is not the only problem, there is also the problem of named loop labels, which are allowed on for _ in loops. Code inside nested loops can break out of outer ones. You would need to put the information necessary to replicate this in the Try-implementing types used by the generated desugaring. The challenge is to convert break/continue/return statements into return <expr>; in such a way as to gettry_for_each to emulate them and hopefully compile efficiently. For reference, here is the current desugaring of for _ in.

Here's an example desugaring using ControlFlow<ControlFlow<integer, integer>, ()>, where the integers represent loop nesting depth, and e.g. return; from the whole function desugars as return Break(Break(0));: playground, plus a println-less version with a silly benchmark that probably doesn't tell us anything since there's nothing to inline.

What is the benefit of "optimising" to a call of try_foreach?

This discussion about for loop desugaring seems very off-topic for this tracking issue. For in-depth discussion on that, please open a topic on https://internals.rust-lang.org, or a new issue on https://github.com/rust-lang/rust.

@tmccombs It gives you internal iteration for more complicated iterators like Chain, rather than calling the outermost next() each time. But while I think that's a useful transformation under user control, I'm skeptical about having the compiler do it.

It took me a really long time for me to wrap my head around this. I was really tripped up on the word "residual", and now I still think that word is unhelpful. It all clicked for me when I realized that Output maps to ControlFlow::Continue and Residual maps to ControlFlow::Break. And Try is, in essence, just Into<ControlFlow>. So I think we should capitalize on the cohesion with ControlFlow by just using the same names.

trait Try: FromBreak<Self::Break> {
    type Continue;
    type Break;
    
    fn from_continue(c: Self::Continue) -> Self;
    fn branch(self) -> ControlFlow<Self::Break, Self::Continue>;
}

Commenting on the bullet of "Decide whether to block return types that are FromResidual but not Try", I have a use case in an error handling system for a parser:

impl<V, C: catch::Catchable> std::ops::Try for GuardedResult<V, C, join::Joined> {
    type Output = V;

    type Residual = GuardedResult<V, catch::Uncaught, join::Unjoined>;

    fn from_output(output: Self::Output) -> Self {
        //
    }

    fn branch(self) -> std::ops::ControlFlow<Self::Residual, Self::Output> {
        todo!()
    }
}

impl<V> std::ops::Residual<V> for GuardedResult<V, catch::Uncaught, join::Unjoined> {
    fn from_residual(r: GuardedResult<Infallible, catch::Uncaught, join::Unjoined>) -> GuardedResult<V, catch::Uncaught, join::Unjoined> {
        GuardedResult { _c: PhantomData::default(), _j: PhantomData::default(), ..r }
    }
}

/// the struct in question
pub struct GuardedResult<V, C: catch::Catchable, J: join::Joinable> {
    value: Option<V>,
    root_error: Option<ParseResultError>,
    cascading_errors: ErrorSet,

    solution: SolutionClass,

    /// If this error has been caught and is planned to be locally handled, this is true
    caught: bool,

    _c: PhantomData<C>,
    _j: PhantomData<J>,
}

I want to be able to force the user to "deal with" an error by using functions implemented on GuardedResult<V, C, join::Unjoined> and GuardedResult<V, catch::Uncaught, J> in order for them to be able to create a GuardedResult<V, Caught, Joined>. I only want to allow the ? operation on GuardedResult<V, Caught, Joined>, but as soon as the error has been bubbled have it revert to GuardedResult<V, Uncaught, Unjoined> so that the next function up is forced to also try to deal with it. Unfortunately having Try: FromResidual forces me to have the type returned by the function be both Joined and Caught, which I don't want to add for the reasons above.

I only want to allow the ? operation on GuardedResult<V, Caught, Joined>, but as soon as the error has been bubbled have it revert to GuardedResult<V, Uncaught, Unjoined>

What if you have the function return the latter, and implement FromResidual for it, but not Try?

Then they'll have to do something to the function return value to convert it to the former -- which implements Try too -- before they can ? it.

I think that's roughly what I've done below, but IMO it is somewhat more complicated to write than the simple "divergent residual" code above:

#[derive(Default)]
pub struct GuardFlagsImpl<C: catch::Catchable, J: join::Joinable> {
    _c: PhantomData<C>,
    _j: PhantomData<J>,
}

trait GuardFlags: Default {
}

impl From<GuardFlagsImpl<catch::Caught, join::Joined>> for GuardFlagsImpl<catch::Uncaught, join::Unjoined> {
    fn from(v: GuardFlagsImpl<catch::Caught, join::Joined>) -> Self { Default::default() }
}


impl<V, G: GuardFlags> std::ops::Try for GuardedResult<V, G> {
    type Output = V;

    type Residual = GuardedResult<V, G>;

    fn from_output(output: Self::Output) -> Self {
        todo!()
    }

    fn branch(self) -> std::ops::ControlFlow<Self::Residual, Self::Output> {
        todo!()
    }
}

impl<V, G: GuardFlags, B: From<G> + GuardFlags> std::ops::FromResidual<GuardedResult<V, G>> for GuardedResult<V, B> {
    fn from_residual(r: GuardedResult<V, G>) -> Self {
        GuardedResult {
            gf: From::from(r.gf),
            ..r
        }
    }
}

impl<C: catch::Catchable, J: join::Joinable> GuardFlags for GuardFlagsImpl<C, J> {}

pub struct GuardedResult<V, G: GuardFlags> {
    value: Option<V>,
    root_error: Option<ParseResultError>,
    cascading_errors: ErrorSet,

    solution: SolutionClass,

    /// If this error has been caught and is planned to be locally handled, this is true
    caught: bool,

    gf: G,
}

This imitates the approach I see in Result<V, E> for converting E from one error to another

I just reread what you said earlier, and I tried doing that but ran into a roadblock: FromResidual requires Self: Try

#[unstable(feature = "try_trait_v2", issue = "84277")]
pub trait FromResidual<R = <Self as Try>::Residual> {
    /// Constructs the type from a compatible `Residual` type.
    ///
    /// This should be implemented consistently with the `branch` method such
    /// that applying the `?` operator will get back an equivalent residual:
    /// `FromResidual::from_residual(r).branch() --> ControlFlow::Break(r)`.
    /// (It must not be an *identical* residual when interconversion is involved.)
    ///
    /// # Examples
    ///
    /// ```
    /// #![feature(try_trait_v2)]
    /// use std::ops::{ControlFlow, FromResidual};
    ///
    /// assert_eq!(Result::<String, i64>::from_residual(Err(3_u8)), Err(3));
    /// assert_eq!(Option::<String>::from_residual(None), None);
    /// assert_eq!(
    ///     ControlFlow::<_, String>::from_residual(ControlFlow::Break(5)),
    ///     ControlFlow::Break(5),
    /// );
    /// ```
    #[lang = "from_residual"]
    #[unstable(feature = "try_trait_v2", issue = "84277")]
    fn from_residual(residual: R) -> Self;
}

If this requirement was changed, though, that would work fine for my purposes (I can't realistically stop the consumer of the library from intentionally abusing return types)

FromResidual requires Self: Try

It doesn't, actually. Only the default needs that. If an impl specifies the generic type parameter, rather than using the default, then the Self type doesn't need to be Try.

Quick demonstration:

#![feature(try_trait_v2)]

pub struct Foo(bool);
impl std::ops::FromResidual<Option<std::convert::Infallible>> for Foo {
    fn from_residual(_: Option<std::convert::Infallible>) -> Self { Foo(false) }
}

pub fn demo() -> Foo {
    Some(1)?;
    Foo(true)
}

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=63baa3bdff6c7fc6dbc6162874a4793c

Neat, that works. It does come at the cost of requiring a potentially "wrong" FromResidual impl to exist.

Take https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=41467f8b6250614106f7b5ab13655e12 as a simple(ish) example.

Currently, I'm forced to add a
image
block even though I never want a Foo to be possible to construct from a Foo residual

@szbergeron I tried to get your first example to work and came up with:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=21ae1121a515b43d206b21272f146944
A problem I see with this approach is that we can't enforce every property we want via the language directly; the following ones conflict:

  • the return type should be GuardedResult<_, Unjoined> to force the user to handle the result
  • the function itself shouldn't be able to return GuardedResult<_, Unjoined> explicitly

Although it might be possible to hack around this using procedural macros, and maybe two different Try types, to make sure that a function on the inside can only return GuardedResult<_, Joined>, but the outside return value is always GuardedResult<_, Unjoined>...

After having explained the new Try trait to several people, I noticed that it repeatedly comes down to three things that are the main source of confusion:

  1. The names Residual and Output. Especially since Output makes most people think of the 'output' of a function, the return value, which points them in exactly the wrong direction since Output is for the case where ? does not result in returning from a function.

  2. The Try::from_output method. It is mostly relevant for the unstable try {} block, which most are not yet familiar with. Most think of the Try trait as just the trait for the ? operator, which doesn't need a from_output when used in functions.

  3. The FromResidual's default Residual type. It is <Self as Try>::Residual even though FromResidual does not require Try. It is left out in the definition of the Try trait (trait Try: FromResidual { .. }). Making it explicit would make it much clearer that it's a requirement about the conversion from Self::Residual to Self. (trait Try: FromResidual<Self::Residual> { .. })

During the library api meeting we had before merging the RFC, I suggested using the names Break and Continue rather than Residual and Output, and I see it has been suggested by @camsteffen in this thread too. After some experience with using and explaining the new Try feature, I still think these names would work better, and would resolve a lot of the confusion around point 1.

During that same meeting, I also brought up the idea splitting the Try trait into two traits to address confusion source 2. After some experience with this new trait, I am now much more convinced we should do this, as it both makes it easier to explain and removes an unnecessary restriction.

For confusion source 3, I think we can remove the default type and instead write it explicitly in the Try definition.

Here's the idea in more detail:

We split Try into Branch and Try, where Branch represents the ? operator, and Try represents the try block. That way, there is a 1:1 mapping of operators and traits, just like we have for the other operators like Add and AddAssign.

The Branch trait does not require FromResidual (or FromBreak), meaning that it can be implemented for custom error types that you cannot construct, which can potentially be useful for FFI wrappers.

The Try trait now only has the from_output (or from_continue or wrap) method to allow it to wrap the outcome in Ok or Some (etc.), and now requires both Branch and FromResidual<Self::Residual> (or FromBreak<Self::Break>) as super traits.

I think the name wrap rather than from_output could work well, since it does the opposite of .unwrap() on types like Option and Result.

The result, with the renames applied, would look like this:

/// The `?` operator.
trait Branch {
    /// The type of the value to continue with.
    type Continue;

    /// The type of the value to break the control flow with.
    ///
    /// This will be converted through `FromBreak::from_break` into the return value
    /// of the surrounding function or `try` block.
    type Break;

    /// Decide whether to continue or return early.
    fn branch(self) -> ControlFlow<Self::Break, Self::Continue>;
}

/// Conversion of `Branch::Break` to the return type of the function or `try` block.
trait FromBreak<Break> {
    fn from_break(_: Break) -> Self;
}

/// The `try` block.
trait Try: Branch + FromBreak<Self::Break> {
    /// Wrap the final value of a `try` block into the resulting type.
    fn wrap(_: Self::Continue) -> Self;
}

(Side note: An option might be to also split off a Wrap trait, changing Try to trait Try: Branch + Wrap<Self::Continue> + FromBreak<Self::Break> {}, which feels almost like a terse explanation of what a try {} block does. (Or maybe we don't even need a Try trait anymore at that point.))

To explain why Residual Break is Result<!, E> rather than E or Result<T, E>, I usually explain that it is basically the type of Err(e), in which T is not relevant.

Using the name changes proposed above, a more complete explanation could be:


Taking Result as an example, branch() will map a Result<T, E> containing Ok(x) or Err(y) to either continuing with x, or breaking with Err(y). Note that while x is no longer wrapped in Result::Ok, y is still wrapped in Result::Err. If r is Ok(x), r? will evaluate to x (unwrapped!), but if r is an Err(_), the surrounding function will return with an Err(_) (still wrapped!).

The types of the two possible outcomes, x and Err(y), are called Continue and Break. Since the type of x is not relevant for Err(y), Break is of type Result<!, E> rather than Result<T, E>. This break value is converted into the return type of the surrounding function or try block through the FromBreak trait.

Keeping the Err(_) wrapper around in the Break value is important to remember it came from a Result and represents an error. This prevents it from getting accidentally getting used as another Break type that does not represent errors.

Any Result<_, E> implements FromResidual<Result<!, E>> such that you can apply ? to a Result<i32, E> even when the surrounding function or try block returns a Result<String, E>.

The resulting value of a try block is automatically wrapped in Ok through the Try trait's wrap method.

I would recommend avoiding the name Continue because there is already a continue in Rust and IIUC it is completely unrelated.

In my opinion the most understandable solution is to use Result in the API because everyone who uses rust already understand what it means and it means that implementing this trait for a custom type is as simple as mapping it to Result. It seems like we are favouring the built-in type but I don't think that it actually important. It is just that it happens to be the purest incarnation of the concept. But I know moved away from that with v2 so maybe I missed the important reason.

In my opinion the most understandable solution is to use Result in the API

Then we'd be back at 'Try trait v1' and its problems. See the motivation section of the v2 RFC: https://rust-lang.github.io/rfcs/3058-try-trait-v2.html#motivation

Maybe I disagree with the motivations listed. To me the From part makes sense and I would stick with another trait. But I think using Result in the API of that trait still makes sense.

@kevincox you think that returning an Err when something succeeds is less confusing than the "Continue" terminology?

Personally, I think that is much more confusing. And from what I've read on this issue and related issues, I think a lot of people would agree with me.

I would recommend avoiding the name Continue because there is already a continue in Rust and IIUC it is completely unrelated.

It's really not a problem, it's follow the same concept of continue keyword, and we already have a Continue with ControlFlow without any problem of confusion with keyword continue

@m-ou-se Perhaps I might have missed the discussion, but I could not find the reason why we want to exclude from_break in the Try trait. My thought is merging FromBreak and Try into a single one would be much clearer to me, where the API would look like the following.

/// The `?` operator.
trait Branch {
    type Continue;
    type Break;
    fn branch(self) -> ControlFlow<Self::Break, Self::Continue>;
}

/// The `try` block.
trait Try: Branch {
    fn from_continue(_: Self::Continue) -> Self;
    fn from_break(_: Self::Break) -> Self;
}

@nyuichi Splitting FromBreak allows you to have impl FromBreak<T> for multiple T.

@m-ou-se I've been thinking more about this, and I think where I really want to hear more is from people who actually have this scenario:

meaning that it can be implemented for custom error types that you cannot construct, which can potentially be useful for FFI wrappers

Because AFAIK that's currently only a hypothetical, not something that I've seen people doing to know how much of an issue it is. For example, windows::core::HRESULT can easily implement both the introduction and elimination directions.

The reason that's important to me is that the option for "you can create it with from_continue or from_break but you can't actually branch it" would also be a useful direction.

For example, I could imagine writing something like this

#[test]
fn yes_it_works() -> QuestionMarkIsUnwrap {
    foo()?;
    bar()?;
    QuestionMarkIsUnwrap::SUCCESS
}

Where there's no need for QuestionMarkIsUnwrap to support ? -- it's fine just having from_output(()) and from_break(impl Debug).

(Coupled with try{} that type might also make a great rustdoc default main function return type.)

Such a design might then have a structure like

trait TryBlock {
    type Continue;
    fn from_continue(_: Self::Continue) -> Self;
}

trait FromBreak<B> : TryBlock {
    fn from_break(_: B) -> Self;
}

trait Branch : FromBreak<Self::Break> {
    type Break;
    fn branch(self) -> ControlFlow<Self::Break, Self::Continue>;
}

The bound on FromBreak could also be on Branch instead, with FromBreak having no supertrait. I haven't thought through those implications in detail. But part of me thinks that it would be good to have create-from-short-circuit-in-? only work on types that are also valid for create-from-continue-in-try{}.

Interesting! I'll try to think of an example.

@m-ou-se I've been thinking more about this, and I think where I really want to hear more is from people who actually have this scenario:

meaning that it can be implemented for custom error types that you cannot construct, which can potentially be useful for FFI wrappers

If I remember correctly, @dtolnay also thought this might be a realistic scenario. @dtolnay, do you have of any example of this?

commented

I don't like the Residual terminology, it feels very abstract and doesn't have any precedent in other languages or the literature (I have no problems with Output though). From that PoV the names Try::Continue and Try::Break indeed make more sense. However, ControlFlow already has Continue and Break variants, which makes the terminology strongly confusing for me. I would expect that for ControlFlow or a similar type I would just map ControlFlow::Continue to Try::Continue and ControlFlow::Break to Try::Break, but of course that's not the way it works. The asymmetry in the definition and usage of those two associated types is very jarring, and I feel it would be a point of confusion. The names Try::Output and Try::Residual are much better from that view: Output is a very natural thing that I want the try ? operator to return (not "throw out of function", just return like a normal operator, like a dereference or an unary minus), while Residual is something slightly esoteric and complicated which I need to look up in the docs and give some special thought. The name Output is especially great IMHO, since it directly describes the output of the operator, just like Add::Output (addition operator), Deref::Output (reference operator) or FnOnce::Output (call operator).

I'm all for a better name than Residual, but I would want it to be just as asymmetric and without wrong connotations.

I am also very wary of the ideas to split the trait further. Yes, it gives more flexibility, but it also gives more moving parts, which means more stuff to wrap the head around and more ways to subtly break the implementation. In particular, it feels very wrong to split try {} and ? into separate parts. This would mean that someone could implement one but not the other, which would be very weird. Why can I use ? to early-return from a function, but I can't do the same in a try block? Why would I ever want to implement success-wrapping in a try {} block without the possibility of using try operator?

This is reminiscent of splitting addition (and other arithmetic operations) into Add and AddAssign, and I am very unhappy with it. I know all the reasons why they are separate, but it's still a bunch of small and large papercuts in the language for my use cases. It means that I must implement more stuff. If I must support operations on both values and references, then there is even more boilerplate to add, and it is a pain to use those bounds in generic code. For all I care a += b is syntax sugar to avoid writing a = a + b (where a and b can be very long and complex identifiers, or even expressions, so that sugar is very worthwhile). But if I'm writing generic code, then I must choose between Add or AddAssign or Add + AddAssign bounds (and it gets even worse with operations on references). If I choose Add, then I cannot use += sugar in the code. If I require AddAssign, then it's one more thing that the consumer needs to worry about (and if they want to use a foreign type which doesn't impl AddAssign, they're stuck). And in any case I must deal with the possibility that a = a + b and a += b could be doing very different things.

The same kind of troubles will likely follow excessive splitting of Try. But while the Add/AddAssign split is inevitable due to the language semantics (+= operates on places and thus must use a &mut, which we can't get from Add without cloning), the splitting of Try feels like overengineering, rather than solving a pressing problem. The possibility of enabling esoteric use cases will cause trouble in rather standard generic code.

It also makes no sense to split try {} and ? from the PoV of effect semantics. From that PoV try is an effect (fallibility), with the normal evaluation producing the Output, and the side effect produces a thrown exception, which can either be propagated to the caller via the ? operator or explicitly handled with a match. It is similar to async {}, which describes the asynchrony effect, or the sequence effect captured by the Generator trait (no current special denotation on blocks). With asynchrony, the side effect consists of execution suspension with no value, and the side effect can be propagated to the caller via .await operator, and the effect handling is performed by the executor. With generators, the side effect consists of an extra returned value, and can be propagated upwards with yield expressions (no special effect handlers). Both effects allow resumption after handling the effect, but that is the detail of their semantics rather than something inherent to effects.

From that PoV splitting try {} and ? into separate parts makes as much sense as splitting async {} and .await into separate parts, or splitting yield from a normal return of value from the generator. Which is, not any sense at all. What would a construct like that even mean? On the other hand, the Try/FromResidual pair is much closer to the effect/handler pair: the Try trait describes the possible side effects (either producing a value normally or returning with an exception), while FromResidual turns the weird side effect (producing a Residual) into a normal value Self: Try which can be consumed by the calling code.

I also tried applying the "present in nightly" try_trait_v2 traits to one of my projects: fogti/gardswag@0923d8d
For me, the need to duplicate part of the implementation ( From<Option<Result<_,_>>> vs the FromResidual + Try implementations) was a bit annoying, it wasn't clear to me how that should be avoided (it was necessary bc of usage in line 313, to convert in case that the ? operator isn't used), except via try blocks.

It's look like curent FromResidual is just the From trait. Why do we need this trait if From is mostly the same ? Unless I miss something they shouldn't have a problem with conflit implementation cause a crate that would have a type that implement Try would by definition own the type.

@Stargateur Unfortunately, that causes conflicts with using ! in the residual type:

#![feature(never_type)]

pub trait From2<T>: Sized {}

// Default impl in the standard library
impl<T> From2<T> for T {}

// Conflicts with the impl above, since T can be !
impl<T> From2<Option<!>> for Option<T> {}

// Conflicts with the impl above, since T can be !, and F can be E
impl<T, F: From2<E>, E> From2<Result<!, E>> for Result<T, F> {}
Errors
error[E0119]: conflicting implementations of trait `From2<Option<!>>` for type `Option<!>`
 --> src/lib.rs:9:1
  |
6 | impl<T> From2<T> for T {}
  | ---------------------- first implementation here
...
9 | impl<T> From2<Option<!>> for Option<T> {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Option<!>`

error[E0119]: conflicting implementations of trait `From2<Result<!, _>>` for type `Result<!, _>`
  --> src/lib.rs:12:1
   |
6  | impl<T> From2<T> for T {}
   | ---------------------- first implementation here
...
12 | impl<T, F: From2<E>, E> From2<Result<!, E>> for Result<T, F> {}
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Result<!, _>`

For more information about this error, try `rustc --explain E0119`.

(see playground)

Marking design concerns because there's still on-going discussion here about trait structure. I don't think there's concerns about the general idea of this (coloured residuals, matching on controlflow, etc), so I don't think there's foundational concerns here, but future work on try blocks might also want to add a few more bounds on associated types here.

(Basically this is the best S-tracking fit, since it's implemented but doesn't seem exactly ready-to-stabilize yet.)

Is it possible to stabilize some subset, like enough to write your own Iterator::try_fold, while leaving those design concerns open in unstable space? You wouldn't be able to impl Try for ... until it's finished, but maybe we can use it a little more...

I think Try::{self, Output, from_output} would be enough, and of course the ? operator we've long had.

I think Try::{self, Output, from_output} would be enough, and of course the ? operator we've long had.

Here's what it looks like for rayon's implementation of try_* methods to use just that subset:
rayon-rs/rayon@master...cuviper:rayon:unstable-try

Is it possible to stabilize some subset, like enough to write your own Iterator::try_fold, while leaving those design concerns open in unstable space? You wouldn't be able to impl Try for ... until it's finished, but maybe we can use it a little more...

This trait is core to the Rust language, I think it shouldn't be rushed. Specially #84277 (comment) suggest change that are not compatible with your suggestion, we should test it before commit to anything.

I agree it shouldn't be rushed, but I'm trying to give it some forward momentum. This stuff has been around in stable use for a long time -- ? since 1.13, try_fold since 1.27 -- so I don't think anyone can accuse it of moving too quickly. Right now we seem to be stuck in a mostly-bikeshed limbo, although there are some functional aspects still being hammered out too. "Perfect is the enemy of good," but I hope an interim goal like "make it possible to write Iterator::try_fold" will help make progress.

Specially #84277 (comment) suggest change that are not compatible with your suggestion

In that proposal, we would need Try, Try::wrap (rename of from_output), and access to the Branch::Continue associated type -- I think you can still name that in constraints like Try<Continue = something> or T: Try, T::Continue: Debug, without directly naming Branch. (I know that works with Iterator subtraits for naming Item, at least.)

I do like the interim goal!

I'll note that it's actually possible to (indirectly) get to Try::from_output on stable today:

macro_rules! try_from_output {
    ($e:expr) => {
        std::iter::empty::<std::convert::Infallible>()
            .try_fold($e, |_, x| match x {})
    }
}

https://play.rust-lang.org/?version=stable&edition=2021&gist=9c3540cb33148314790bd75840aab638

Another option for letting people call it without stabilizing trait details would be to stabilize try{} blocks -- that's actually how the standard library calls it:

(That was convenient for the v1 → v2 change as it meant the library code didn't need to change, just the try{} desugaring in the compiler.)

So I think the core thing that'd be needed for an interim bit is just a way to let people write the equivalent of R: Try<Output = B>. One version of that we could consider is adding something like

// in core::iter

pub trait TryFoldReturnType<Accumulator> = Try<Output = Accumulator>;

so that users can write

    fn try_fold<B, F, R>(&mut self, init: B, mut f: F) -> R
    where
        Self: Sized,
        F: FnMut(B, Self::Item) -> R,
        R: TryFoldReturnType<B>,
    {

and if we change how that's implemented later, great. (We could even deprecate or doc-hide it later, should another option become available, like we did with https://doc.rust-lang.org/std/array/struct.IntoIter.html#method.new.)

Idea written as trait alias for communication simplicity. It'd probably want to be a trait with a blanket impl and an unstable method so it can't be implemented outside core, or similar, if we wanted to actually do this, because we probably don't want to expose trait aliases to stable consumers just yet.

"make it possible to write Iterator::try_fold" will help make progress.

I don't understand what you mean by that, you can use ControlFlow of std that is stable.

Edit: Ah yes the Try trait for the generic.

"Perfect is the enemy of good,"

I agree I just think naming is important, the feature of current Try is fine for me.

"make it possible to write Iterator::try_fold" will help make progress.

I don't understand what you mean by that, you can use ControlFlow of std that is stable.

I mean for your own custom type implementing Iterator, it should be possible to write a custom version of the try_fold method, just like we do for the implementations for Chain, FlatMap, etc. At minimum, that means you must be able to match the constraints of that method, R: Try<Output = B>.

I used Try in a project and there is one thing that I would like but I don't think it's possible. I also don't know if it's needed. My problem is that like iterator I end up have duplicate function, one variant for thing with Try and thing without. My concern is both about user experience and performance. It would be nice that we could impl Try<Residual = Infallible> for every T. I think is impossible since we would end with conflict implementation. I end up with something like this:

#![feature(try_trait_v2)]

use std::convert::Infallible;
use std::ops::{ControlFlow, FromResidual, Try};

pub struct NoFail<T>(pub T);

impl<T> FromResidual for NoFail<T> {
    fn from_residual(_: Infallible) -> Self {
        unreachable!()
    }
}

impl<T> Try for NoFail<T> {
    type Output = T;
    type Residual = Infallible;

    fn from_output(output: Self::Output) -> Self {
        Self(output)
    }

    fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
        ControlFlow::Continue(self.0)
    }
}

enum Foo<T, E> {
    T(T),
    E(E),
}

impl<T, E> FromResidual<Infallible> for Foo<T, E> {
    fn from_residual(_: Infallible) -> Self {
        unreachable!()
    }
}

impl<T, E> FromResidual for Foo<T, E> {
    fn from_residual(foo: Foo<Infallible, E>) -> Self {
        match foo {
            Foo::T(_) => unreachable!(),
            Foo::E(e) => Foo::E(e),
        }
    }
}

impl<T, E> Try for Foo<T, E> {
    type Output = T;
    type Residual = Foo<Infallible, E>;

    fn from_output(output: Self::Output) -> Self {
        Foo::T(output)
    }

    fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
        match self {
            Foo::T(t) => ControlFlow::Continue(t),
            Foo::E(e) => ControlFlow::Break(Foo::E(e)),
        }
    }
}

fn foo<T, U, E>(u: U)
where
    U: Try<Output = T>,
    Foo<T, E>: FromResidual<U::Residual>,
{
}

fn main() {
    foo::<i32, _, ()>(NoFail(42));
    foo::<i32, _, ()>(42);
}

From the user point of view that not better than Ok(42), from a performance point of view I don't know, I expect Rust is able to optimize a use of Try that can't fail. I would like to know if it's possible to write foo::<i32, _, ()>(42);.

My second question is: what is the recommandation about duplicate code, should we have two variants for most function or should we only propose the "Try" version ? It's look similar to async problem with "What color is your function ?".

I haven't followed the discussion closely, but I have just run into a situation where I think this would help.

Basically I have an enum Response of possible responses from a server, including successful responses and error responses. It would be great if I was able to use the ? operator to easily "short circuit return" when certain other function return errors. For instance, if I get a Err(e) of some certain type Result<T, E>, I would like ? to map that to a certain variant in my response enum and return that (while an Ok(t) should just continue execution).

A temporary workaround that my team is currently using is the somewhat bizarre type of Result<Response, Response>. This allows one to use the ? operator to return a response of a certain variant in case of Err but also return a response in case of Ok. However you then have to unwrap_or_else(|e| e) the result in order to get either the Ok or Err response out.

Again, haven't followed closely but would love if this feature would allow this use case to simply use Response as a return type rather than Result<Response, Response>! :)

A temporary workaround that my team is currently using is the somewhat bizarre type of Result<Response, Response>. This allows one to use the ? operator to return a response of a certain variant in case of Err but also return a response in case of Ok. However you then have to unwrap_or_else(|e| e) the result in order to get either the Ok or Err response out.

I would advice if you don't want use nightly to split you Response into two, one for Ok, one for Err. If you have several ok value or err value use two enums.

enum ResponseOk { Todo };
enum ResponseErr { Todo };

then use Result<ResponseOk, ResponseErr> that way better than your current approach and will fit nicely when Try trait is stable. If you end up saying "but sometime a response can be either Ok or Err" then the try trait is probably not what you seek (thus I could be wrong here).

Unfortunately the Response type is an enum coming from autogenerated code that is generated by prost, i.e. a Protocol Buffers type definition. So changing it is not so easy.

... then use Result<ResponseOk, ResponseErr> that way better than your current approach and will fit nicely when Try trait is stable.

Would the try trait not be able to handle an enum like Response? For instance, imagine this:

enum Response {
    Success,
    NotFound,
    InvalidRequest,
    InternalError,
}

With the try trait fully part of the language, would I not be able to write short-circuiting return behavior for this type, using the ? operator? If not, I'd be kinda worried about the design of the trait as I would think this use case would be the whole point almost. I mean, it's just a Result with more than two possibilities. I feel like that should be handled by this feature.

I think I'm okay with using the Result<Response, Response> workaround until the try trait lands (assuming it would work for my use case).

@scottmcm asked me to share my experience with using these traits. I've been writing some Rust for a few years, but not as comfortable with it as with most other languages I know, but maybe that's useful.

My goal was to allow devs to use the ? operator with any error type to convert to a u32 (more specifically, a UINT or unsigned int in C / Win32) as seen here: https://github.com/heaths/msica-rs/blob/db652f4c85123171c9f0f6acf406547655934f13/src/errors.rs#L153-L160. Windows Installer custom actions all have a (C) form of extern "C" UINT FuncName(MSIHANDLE hSession). Supported error codes from a custom action are limited, with all other values effectively being mapped to 1603: https://github.com/heaths/msica-rs/blob/db652f4c85123171c9f0f6acf406547655934f13/src/errors.rs#L197-L203.

It wasn't too hard to discover these traits, but it wasn't clear what a "residual" was ("output" was obvious) and needing a newtype for a NonZeroU32 was initially nonobvious; though, it made since once I realized 0 shouldn't result in an Error so should be an otherwise impossible case. It wasn't until I found https://rust-lang.github.io/rfcs/3058-try-trait-v2.html#implementing-try-for-a-non-generic-type that all the pieces started falling into a place. A good example like that might be good on either (or both) of the traits.

Once implemented FromResidual for my custom error type, I was still not able to figure out how to return a u32 (effectively) for any error. This is still an ongoing problem. To fulfill my goal, I just want any error to ideally map to one of the handful of values a custom action can return, or at the very least just return 1603 (ERROR_INSTALL_FAILURE). Though not ideal, at least a workaround exists: since I implement From<std:error::Error> for my Error type (effectively like #[thiserror::from]), one could do:

CString::new("t\0est").map_err(|err| err.into())?

Having to map all errors is less than ideal, though.

One nit: since this largely seems related to mapping std::result::Result<T, E> to some arbitrary output, it seems that std::ops::ControlFlow<B, C> should clip the C (Continue) and B (Break) type arguments, which seem to better align with <T, E>; though, this may simply be my own misunderstanding of how this enum is intended to be used. (@scottmcm explained why)

That said, both ControlFlow::Break and ControlFlow::Continue enums seem very intuitive, as was Try::Output. It was only Try::Residual (and, by extension, FromResidual itself) that were not. Reading through the motivation in the RFC, would something related to "error" or even "break" make more sense? In the RFC, the Try trait section reads,

The residual that will be returned to the calling code, as an early exit from the normal flow.

So some word more closely related to "error", "break", or even "exit" might help understanding it better.

May I suggest Unexpected as an alternative to Residual? After reading through the RFC thoroughly (twice and early on), I think Residual conveys the meaning best, although English is not my first language. But the rationalization for Residual is a little hard to follow. Unexpected might be a better choice there because that is easier to model mentally, and I haven't seen it suggested before.

I think Unexpected has the same problem as Error, where it can make it awkward to use it in places where it is actually expected that it is used. Perhaps "Exceptional" would work better, as it is an exception to the normal flow? But then you might have some baggage from exceptions in other languages.

The RFC cites the Oxford definition as:

a quantity remaining after other things have been subtracted or allowed for

That aligns with my definition, but to me the intuitive interpretation was "extracting the diverging cases and leaving the remaining cases to continue execution", thus outputting (returning) the diverging type and continuing execution with what remained, the residual. If this is specifically for the ? operator, could one use more explicit and less confusable term like Diverging?

As a follow-up to my experience above wherein my goal was to allow for any error to effectively return ERROR_INSTALL_FAILURE (1603), I wanted to support the case where a callee could return a wrapped u32.

The most obvious way was with specializations as seen here: heaths/msica-rs#22. I'm considering not supporting this case, though, because I think the use cases are small enough not to warrant yet another unstable feature that seems even less likely to stabilize.

Perhaps to facilitate handling specific residuals but falling back to more general one, could core at least support specialization for cases like this? It seems that's the direction being considered, that only core could use specialization because of unsound behaviors in general.

It sounds like both @Victor-N-Suadicani's example and @heaths's example are cases where the desire is for the actual result type to be different than the type that provides the FromResidual implementation.

I ran into a similar problem when trying to write a type that would allow x? to panic on failure (effectively becoming sugar for unwrap()). There needs to be some type Unwrapped<T> providing FromResidual<E: Display> so that from_residual can panic. But this unfortunately means that Unwrapped<T> needs to be actually exposed in the function signature. Playground link.

In all these cases, effectively there is no distinction between an actual output return and a residual return.

Another use case I can think of is defaulting. Imagine:

struct OrDefault<T>(T);

impl<T: Default> FromResidual<Option<!>> for OrDefault<T> {
  fn from_residual(o: Option<!>) -> OrDefault<T> {
    Some(Default::default())
  }
}

It seems to me that the core disconnect here is that the return type of the function is expected to contain the error handling logic, but these kinds of use cases show that this isn't true. There are multiple plausibly valid ways to convert between, say, Option<!> and T, or Result<_, !> and T. And the existing syntax has no way to disambiguate between them, so there is no way it can possibly work.

So I think the only reasonable way to do this kind of think is with more syntax, probably an inner try block or some sugar, but I'm not even sure how one would annotate the types to make that work.

We have GATs now, so maybe we can make the design less awkward? Current type for std::array::try_from_fn doesn’t fill me with joy, and it seems like it could be more readable:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=51e5635ce77b7cc3672d8c69223da5fb

@GoldsteinE I wrote some notes about why I didn't make it a GAT over in Residual's tracking issue, #91285 (comment)

@scottmcm That's reasonable. I feel like current signatures of try_ functions seriously hinder learnability though. Maybe we could have additional trait with blanket impl (as in my playground) or some other kind of sugar?

Additional thought: I think people would probably write their own try_ function more often than new Try types and it shouldn't require to write this whole incantation every time (even the standard library doesn't do it — there's a private type alias to make it easier).

Sorry to interrupt but a quick question. Can this trait Try be automatically implemented from Default?

So some word more closely related to "error", "break", or even "exit" might help understanding it better.

premature, slink

I want to make the argument to unwind most of the residual based try API. I do not know how much can be unwound, and I not know how much should, but here is why I'm starting to lean towards Try in this form being a mistake.

Let me state that I really appreciate being able to use ? on Option and Result, and I also agree with the desire that this type of functionality is interesting to have for custom types as well. If the only need was to have these traits be some internal abstractions for that operator I would probably not think that much about it.

However some of the APIs that are currently making their way into the nightly builds are really horrible in terms of type definitions. The original Iterator::try_fold doesn't read all that bad:

fn try_fold<B, F, R>(&mut self, init: B, f: F) -> R
where
    Self: Sized,
    F: FnMut(B, Self::Item) -> R,
    R: Try<Output = B> { ... }

Unfortunately some of the newer APIs require much more involved bounds. A pretty bad example is try_collect and try_reduce:

fn try_collect<B>(
    &mut self
) -> <<Self::Item as Try>::Residual as Residual<B>>::TryType
where
    Self: Sized,
    Self::Item: Try,
    <Self::Item as Try>::Residual: Residual<B>,
    B: FromIterator<<Self::Item as Try>::Output> { ... }

fn try_reduce<F, R>(
    &mut self,
    f: F
) -> <<R as Try>::Residual as Residual<Option<<R as Try>::Output>>>::TryType
where
    Self: Sized,
    F: FnMut(Self::Item, Self::Item) -> R,
    R: Try<Output = Self::Item>,
    <R as Try>::Residual: Residual<Option<Self::Item>> { ... }

As a somewhat seasoned Rust developer it takes me too long to understand what is going on here, and it is even harder to explain to someone not particularly familiar with Rust. The Residual stuff also makes itself out via some compiler errors (even on stable) and I doubt that users are not at least in parts confused:

1 | fn foo() -> Result<(), ()> {
  | -------------------------- this function returns a `Result`
2 |     Some(42)?;
  |             ^ use `.ok_or(...)?` to provide an error compatible with `Result<(), ()>`
  |
  = help: the trait `FromResidual<Option<Infallible>>` is not implemented for `Result<(), ()>`
  = help: the following other types implement trait `FromResidual<R>`:
            <Result<T, F> as FromResidual<Result<Infallible, E>>>
            <Result<T, F> as FromResidual<Yeet<E>>>

Why do I need to know what a FromResidual is? The documentation for all of this machinery also does not really do a good job at explaining it either. This is the documentation of the associated Residual type on Try (with questions of mine added in italics):

The choice of this type is critical to interconversion (why?). Unlike the Output type, which will often be a raw generic type, this type is typically a newtype of some sort to “color” the type so that it’s distinguishable from the residuals of other types. (why? it does not explain the motivation)

This is why Result<T, E>::Residual is not E, but Result<Infallible, E> (again, why? This does not explain the motivation at all). That way it’s distinct from ControlFlow<E>::Residual (where is ControlFlow all the sudden coming from?), for example, and thus ? on ControlFlow cannot be used in a method returning Result (None of this is obvious).

If you’re making a generic type Foo<T> that implements Try<Output = T>, then typically you can use Foo<std::convert::Infallible> as its Residual type: that type will have a “hole” (What is a hole? Why do I need one?) in the correct place, and will maintain the “foo-ness” (What is foo-ness?) of the residual so other types need to opt-in to interconversion. (You completely lost me here)

Obviously with some reading on other parts you can piece together all of this, but man this is a complex system for the supposed simple operation of "unwrap success, propagate and convert error on error".

@mitsuhiko I'm not sure if I necessarily agree with the conclusion, but I am definitely quite annoyed with the type signatures too. Another example is OnceCell::get_or_try_init. Today, its type signature is this:

    pub fn get_or_try_init<F, E>(&self, f: F) -> Result<&T, E>
    where
        F: FnOnce() -> Result<T, E>,

I can pretty much read that almost instantly. It does have generics, but they are succinct and pattern match to extremely widely used patterns.

But in PR #107122, it has been proposed to make this generic to support Try. The proposed type signature is now:

    pub fn get_or_try_init<'a, F, R>(&'a self, f: F) -> <R::Residual as Residual<&'a T>>::TryType
    where
        F: FnOnce() -> R,
        R: Try<Output = T>,
        R::Residual: Residual<&'a T>,

My eyes instantly gloss over. I've been using Rust for almost ten years, and I can barely read this.

It is possible that these sorts of signatures will become so ubiquitous that our eyes will learn to pattern match them. But I'm skeptical. There is a lot going on here. There's three traits. Associated types. Weird as syntax in the type the language. Oodles of symbols. It just does not lend itself to being comprehensible. And even worse, as a member of libs-api, it's really hard for me to even know whether this type signature is correct. Is it covering all the use cases it is intended to correctly? Is it missing any? What are the backcompat hazards? These are all very tricky questions to answer.

Now, the middle road between the complaints I'm lodging here and the Try trait more generally is that we don't actually have to go through and make every one of these methods generic on Try. The problem with that sort of road is that it's like running up an ever increasing hill.

commented

I'm new to rust and have been very much enjoying what I've seen so far. I love the ? operator. I was quite disappointed that I couldn't use it on Option. Then I stumbled across this issue :)

Can someone summarize for me and others that are new to the language like me why adding ? support to Option is so challenging? What are the edge cases that we are struggling to overcome?

From the perspective of a new and naive user, it seems like converting to Error should be simple enough. We already have ok_or and ok_or_else. It seems like we could just bundle that with ?.

(Again I'm naive and new :). I'm sure it's more complex than that)

The new API generalization wordiness all comes from ops::Residual moreso than Try directly, though the two are of course entwined. It's also worth noting that there's an alias used that's not exposed in rustdoc which makes the signatures a bit clearer and a bit less cluttered:

fn try_collect<B>(&mut self) -> ChangeOutputType<Self::Item, B>
where
    Self: Sized,
    <Self as Iterator>::Item: Try,
    <<Self as Iterator>::Item as Try>::Residual: Residual<B>,
    B: FromIterator<<Self::Item as Try>::Output>,
;

Just for interest, here's how it could look with associated type bounds:

fn try_collect<B>(&mut self) -> ChangeOutputType<Self::Item, B>
where
    Self: Sized,
    Self::Item: Try<Residual: Residual<B>>,
    B: FromIterator<Self::Item::Output>,

It's still dense, but a lot less noisy; in my opinion it's noticeably clearer what the signature is saying. (Assumes the <Self::Item as Try>::Output associated item disambiguation could be made unnecessary.)

The difficulty fundamentally comes from wanting to rebind to the same try "kind" (i.e. Result versus Option). Without trying so hard to prevent this, the signature can become a lot easier to look at, even without associated type bounds:

fn try_collect<B, R>(&mut self) -> R
where
    Self: Sized,
    Self::Item: Try,
    R: Try<Output = B, Residual = Self::Item::Residual>,
    B: FromIterator<Self::Item::Output>,
;

The problem is that this kills type inference, for a related reason to why try blocks are currently nearly unusable without extra type annotation: you can't derive the output "try kind" uniquely anymore. Rust isn't really able to talk about Result rather than specifically Result<T, E>; that's where the residual comes from, as we can talk about Result<!, E>. (try blocks additionally suffer from error conversion meaning the error type isn't known uniquely until further constrained.)

Working backwards from the error, what might we ideally want it to look like? Perhaps:

1 | fn foo() -> Result<(), ()> {
  | -------------------------- this function returns a `Result`
2 |     Some(42)?;
  |             ^ use `.ok_or(...)?` to provide an error compatible with `Result<(), ()>`
  |
  = help: the trait `Try<Option<_>>` is not implemented for `Result<(), ()>`

(FWIW, I think the offering of other implementations here is just wrong. It depends on what you consider the polarity of the error to be: that the return type can't receive )

That could give us the trait shape of:

pub trait Try<R> {
    type Output;

    fn from_output(output: Self::Output) -> Self;
    fn branch(self) -> ControlFlow<R, Self::Output>;
}

I chose R to signify the "Return" type of the try unit, but if it weren't for the overwhelming std style of single-letter type generics, I'd probably call it Break. I think this supports all of the current cases,

(very shorthand)

// stable, kind preserving
impl Try<ControlFlow<B, _>, Output = C>
for ControlFlow<B, C>;
impl Try<Option<_>, Output = T>
for Option<T>;
impl Try<Result<_, E'>, Output = T>
for Result<T, E>;
impl Try<Poll<Result<_, E'>>, Output = Poll<T>>
for Poll<Result<T, E>>;
impl Try<Poll<Option<Result<_, E'>>>, Output = Poll<T>>
for Poll<Option<Result<T, E>>>;

// unstable, kind preserving
impl Try<Ready<_>, Output = T>
for Ready<T>;

// unstable, kind changing
impl Try<Poll<_>, Output = T>
for Ready<T>;
impl Try<Option<_>, Output = !>
for Yeet<()>;
impl Try<Result<_, E'> Output = !>
for Yeet<E>;

// stable, kind changing
impl Try<Result<_, E'>, Output = Poll<T>>
for Poll<Result<T, E>>;
impl Try<Result<_, E'>, Output = Poll<Option<T>>
for Poll<Option<Result<T, E>>>;
impl Try<Poll<Result<_, E'>>, Output = T>
for Result<T, E>;
impl Try<Poll<Option<Result<_, E'>>>, Output = T>
for Result<T, E>;

I do actually quite like how the purpose of each impl is immediately clear, as opposed to e.g. Ready<T> having a residual of Ready<!> and you having to know that Poll<_>: FromResidual<Ready<!>> to assemble the real functionality there.

It also makes things more deliberate. Poll reused Result's residual and I'm not certain the result was intentionally bidirectional. When utilizing residuals, any other (potentially downstream) types which decide to take the easy approach of using Result as a residual instead of defining a fresh one will now all be interchangable by ?, corresponding to a huge cross product of impls in this flattened Try<R> representation, which could be desirable, but I believe isn't.

This does seem nicer on the surface,


but it doesn't really help at all with the rebind case or try, and in fact makes it significantly worse, since now you need to know the "try output" type in order to determine what the output type of e? is.

Knowing the output type directly from e? is a major reason the residual type is associated with. The counterparty is thus to embrace the rebind and throw GATs at the problem, e.g.

pub trait Try {
    type Output;
    type Break<T>;

    fn from_output(output: Self::Output) -> Self;
    fn branch<T>(self) -> ControlFlow<Self::Break<T>, Self::Output>;
}

but now we've overcorrected and thrown the whole ball of GAT complexities into the mix, plus heavily cut back on actually useful expressivity:

  • Error conversion via From when using ? on Result isn't possible anymore.
  • It's also now impossible to have kind-adjusting ? at all. The Option <-> Result one that accidentally resulted from the v1 Try has been removed, but Poll<Result> <-> Result and Poll<Option<Result>> <-> Result conversations were deliberately added.
  • Additionally, cases like the HResult example from the RFC with restricted Output type domains or which just aren't generic over Output type in the first place no longer fit in with the use of GAT here. These same reasons apply to why ops::Residual is generic rather than ops::Residual::TryType, despite R::Residual::Try<&'a T> reading much better than <R::Residual as Residual<&'a T>>::Try.

With the hindsight of the difficulties try kind conversion and error conversion bring, maybe it would have been better if the Poll kind adjustment was a transpose method and error conversion required .map_err(Into::into). However, error conversion is a huge part of what makes ? convenient to use (even though uses often end up with a .context() style conversion attached anyway), and kind conversion has its motivating examples as well (e.g. a manually packed version of Result for specific T, E).

The last option is to go back towards v1 (using the do yeet terminology):

pub trait Try {
    type Output;
    type Yeet: Yeet<Self>;

    fn from_output(output: Self::Output) -> Self;
    fn branch(self) -> ControlFlow<Self::Yeet, Self::Output>;
}

impl Try<Output = T, Yeet = E> for Result<T, E>;

and perhaps it's okay that this results in being able to ? between Option and Result<_, ()> since you can do yeet () to both, but this still does nothing for the rebind case that ops::Residual exists for.

So I don't think it's possible to cover try without three traits unless you're willing to drop functionality. You need Try to split the control flow, FromResidual to catch the broken value into a different type, and Residual to be able to rebind Try types to different output types. Only the functionality of the third isn't stable already, and it's the most foreign concept to anything currently stable (as it exists only to provide a type level mapping rather than any functionality of the impl type).

I'm quite generic-pilled, and to me the current shape seems fine, although the naming and documentation could certainly be improved. I don't have any good naming suggestions, though. My only remaining relevant observation is that the Residual type/name does feel intrinsically linked to do yeet, but the two are distinct concepts, since you do yeet the data carried by the residual.

Perhaps the answer is to (improve the docs for Try and) drop ops::Residual and any dependent functionality, either solving its problem space with keyword generics/effects or not at all.

Besides the type bounds I think part of the problem here is that "residual" is also an incommon English word that doesn't mean much to non native speakers. I would not be surprised if you say "residual" the only association that people have is the word "waste". But I don't think that is the biggest issue here.

and perhaps it's okay that this results in being able to ? between Option and Result<_, ()> since you can do yeet () to both, […]

On it's own I think being able to go from Option to Result<_, ()> both makes sense and is quite convenient. If that's the main restriction we are dealing with here, I would not have assumed that this needs to be resolved.

but this still does nothing for the rebind case that ops::Residual exists for.

Which I'm not sure what this is trying to address so I cannot give a comment here to it. My observation is that as an outside observer, I do not understand anything what's going on here and I hope I do not have to do a PHD in types to use some of those APIs.

I think the right way to go about this is:

  1. What's the simplest API that only addresses Option and Result
  2. What extra API is needed to allow abstraction over more types
  3. If the complexity for 1. is significantly easier than 2., maybe 2. should not be done.

I can come up with a bunch of quite trivial ways to go about 1, I don't think I could go about 2.

\2. is already done and stable with Poll, though, so not doing it isn't an option.

Rebinding refers to the use in e.g. try_collect, taking some try type (e.g. Result<T, E>) and a new output type (e.g. Vec<T>) to determine a different try type (e.g. Result<Vec<T>, E>).

ops::Residual is only used for rebinding. The FromResidual mechanism isn't all that complicated in actuality, it's just somewhat difficult to explain. It's no more complicated than other uses of newtype wrappers, just obscured a bit by the use of ! rather than fresh types.

That’s why I’m wondering how much could even be unwound. I’m not convinced that there is no path to undo the damage caused by Poll if there was a desire to do so. The treatment of into_iter on arrays comes to mind which had a creative solution that allowed a change that was assumed to be not possible.

One case of concern to me is that this deeply bakes in what feels like an escape hatch for Result, namely the possibility of automatic error conversion via From, which makes type conversions and re-bind prevention imo harder to reason about. I would prefer it if this aspect were realized in a way that doesn't hamper type inference as much (or perhaps make it completely optional via edition and put that abstraction into a completely separate trait). That is, this basically folds multiple conceptually distinct conversions together, but the abstraction doesn't seem particularly fitting (the problems with type inference also make that difficult to use in most cases that involve From, and to me it seems that that should be solved separately if possible (perhaps via syntactic sugar for From::from or such))

Do we need to use the same set of traits for governing the built-in language features (?, try, hypothetical yeet) and for expressing abstractions in library code (try_collect() and company)? If we don't do that, could it reduce the complexity/requirements load on either or both? I'm reminded of the way we have Deref, AsRef, and Borrow as well, which has some downsides, but potentially some upsides also.

(That is, it would carry the tradeoff of further proliferating traits; but you would only have to interact with the ones relevant to your current use case. This was meant as an actual question for my own edification, not a thinly veiled pointed suggestion.)

If I may summarize some of the discussion here, in part for my edification, it sounds like there are several distinct issues being discussed concurrently:

1. Use of the word "residual" in the API

The naming of "residual" for "the thing that gets passed up to the caller with the ? operator." This naming is shown in the Residual associated type on Try, and in the FromResidual trait. The central concern about the name is that it may not be immediately clear, especially to non-native English speakers, what the "residual" of a Try type is. A number of alternative names have been proposed, but there are concerns about implying "error"-ness or "unexpected"-ness, because those implications may not make sense for every context. Hence the relatively-neutral-but-unusual word "residual."

2. The impact of having both the Try and FromResidual traits on the complexity of trait bounds.

Several examples have been given in the thread of new APIs involving these traits which see their signatures become increasingly complex as they incorporate these experimental traits. In particular, additional work is needed to tie the <T as Try>::Residual to the R parameter in the FromResidual trait, to make sure they're the same when needed. It is possible that permitting associated trait bound syntactic sugar would improve this situation, but this may not be considered sufficient.

3. A question of the value of splitting Try and FromResidual in the first place.

Obviously, this was central to the changes from version 1 of the Try trait proposal to version 2. The value of having FromResidual as a super-trait of Try is that any Try-able type can support handling the use of the ? operator on other types, so long that there's an impl FromResidual<SomeOtherType> for MyType. This flexibility can be nice! However, the split between these two traits seems so far to have confused a number of folks.

4. A question over whether the current design can be changed, given its use throughout the standard library.

This API has already impacted the standard library, particularly through various try_* functions, and the FromResidual implementations for Poll. There's a question of how much here can be rolled back. I don't have a clear sense of the constraints / complexity of this task.

5. Concerns about the Infallible stuff appearing in the relevant impls.

As I take it, the use of Infallible in different places has to do with Rust's inability to reason about higher-kinded types as first class citizens. Infallible isn't super common in Rust code today, so people may be surprised by or unfamiliar with it.

6. Concerns about also teaching the helper ControlFlow enum.

The method Try::branch returns a ControlFlow, which means documentation for Try has to teach this type and what it means. Not impossible, but not done super well as-of-yet.

7. Concerns about how well this design plays with Rust's type inference.

Bad-type-inference errors are generally very frustrating to deal with, because (especially without good error messages) you can easily end up in a space where you feel like you're adding random hints to the compiler until it tells you the types are correct. I take it this current API has inference issues, although I don't understand the technical specifics yet.


If I've missed any big topics under discussion which are currently blocking progress on v2 of the Try trait, let me know.

I do notice that a lot of these are education questions, about how easy it will be for people to understand this new API. This touches on documentation, error messages, and the terminology itself.

There are distinct questions about implementation, which basically amount to questions of complexity of bounds and difficulty of correct type inference.

All of that said, I'd like to throw my hat in as someone interested in helping. I have a crate called woah (woah::Result's Try impl) which I am keeping pre-1.0 in part because it really needs the Try trait available to be usable, and I don't want to stabilize with a nightly-only unstable trait being so central to the design.

I'm particularly interested in helping around the documentation / education parts of things. Let me know if there's a good place to start.

commented

A number of alternative names have been proposed, but there are concerns about implying "error"-ness or "unexpected"-ness, because those implications may not make sense for every context.

Seeing this randomly gave me an idea: How about ‘Alternate’?

It seems to fit the ‘neutral’ requirement, not making a particular value judgement beyond ‘different’, and seems to work well in prose, i.e. ‘the alternate path is taken’

I'd say it's also more intuitive for both native and non-native speakers, compared to a slightly odd interpretation of a relatively obscure word

commented

(NOT A CONTRIBUTION)

A small change that I think would make this a lot easier to intuit would be to remove the default value for the generic parameter on FromResidual and change the supertrait bound on Try to Try: FromResidual<Self::Residual>. Some notes:

  1. There's just a lot to parse in FromResidual<R = <Self as Try>::Residual>. I find it hard to even figure out that what I'm looking at is a generic trait with a default value for the parameter, and not something like a supertrait bound.
  2. The alternative I'm proposing seems much more conventional.
  3. I, a user with an above average understanding of the trait system, did not even know it was possible to make the default bound depend on an impl of a trait that isn't a super trait in the way this does here. I doubt 1% of Rust users know this, and I doubt 10% of Rust users would find it easy to understand what's going on in less than a minute of reading the docs.
  4. My understanding is that FromResidual isn't really meant to be used anywhere, rather than Try, and so getting the default parameter doesn't seem to have much benefit.

Frankly, when I read that you had modified the Try trait to add a super bound that looks like FromResidual<R = <Self as Try>::Residual>, my immediate reaction was very negative. When I read through the RFC, I was more convinced that the actual structure of the interfaces has unique utility. But this specific quirk of it I think doesn't carry the weight of the cognitive load it adds to the interface.


I also think that "residual" is an unclear name and this isn't an area where Rust needs any more unique terms of art, but as a rule I now try to keep my comments on the lowest rung of Wadler's ladder possible so I won't push that further.


As another note, I think the desire the "generalize" APIs that currently involve closures returning Result to support any try type is a mistake in itself. I don't think there's a way to not get highly generic, inscrutable type signatures (regardless of the decision on the definition of Try), and I don't think there's enough utility in doing that to justify the cognitive overhead of those signatures (since Try types should have a pretty straightforward conversion to Result if you really need to use some other Try type in that case). PRs like #107122 should be closed as undesired.

Okay, it does seem like the question of whether the flexibility afforded by FromResidual is worth it is the most fundamental question. Improving naming or documentation around a feature are secondary to deciding if the feature should remain.

To help inform that discussion, I'd like to work on answering some questions:

  • How much more complex, in real world codebases, do type signatures become when incorporating the additional flexibility of FromResidual?
  • In practice, how much usage of ? and Try appear to use non-Result types?
  • What specific additional generic powers become available in the presence of FromResidual?

Each of these can at least attempt to be answered by surveying available Rust codebases, something which I believe has already been done at least in part by others. I'd like to go through the discussions to identify any samples already taken, and likely add more samples myself, to help everyone involved have more concrete reference points for the benefits, additional complexity, and possible demand for this feature.

In practice, how much usage of ? and Try appear to use non-Result types?

Asking this question for unstable functionality rarely gives useful answers, because for most use cases, using a stable-compatible workaround is preferable to using unstable functionality.

There are five types which can be stably used with ?: Result<_, _>, Option<_>, ControlFlow<_, _>, Poll<Result<_, _>>, and Poll<Option<_>>.

All Try types will look "Result-ish," because the try operation is monadic, and Result is a canonical monad.

What specific additional generic powers become available in the presence of FromResidual?

The primary purpose of FromResidual is that it defines what types you can use ? on in a function which is returning Self.

The biggest incidental complexity of the residual system imho is the fact that there isn't a one-to-one mapping between Try and residual. The residual (definition "the part left behind") is supposed to represent the try type without the output case, so it's unfortunate that custom impls are getting implicitly nudged into just using Result<!, E> or Option<!> instead of actually encoding their type's residual.

In implementing FromResidual<Result<!, E>> for a type, you are stating that you should be able to ? a Result<_, E> in functions returning that type. That this meaning is obscured behind an intermediate is unfortunate.

Something along the lines of FromResidual needs to exist, though, because it's allowed to apply ? to a nonequal type, both in the simple "stem preserving" case of Result<T, E> -> Result<T, impl From<E>> and in the "stem changing" case of Poll's transparent Try impls.

(Although the latter is somewhat controversial in retrospect and Poll::ready exists unstably to "fix" it, it's stable and changing it is essentially1 a nonstarter.)

How much more complex, in real world codebases, do type signatures become when incorporating the additional flexibility of FromResidual?

Frankly, FromResidual probably shouldn't show up generic signatures. The only time it makes any potential sense is with a generic return type (to communicate the potential failures the return type needs to accept), and a nongeneric return type of Result accomplishes this just as well, while not having the usability issues inherent to generic return types. And even then you still need to have some additional bound enabling the function to construct a success output.

Basically, being generic over FromResidual is like being generic over From — it doesn't make sense in 99% of cases to be generic in that direction. That direction is a trait because it makes the usefully generic direction of Into easier to express interconversions for.

The trait which would actually be appropriate for generic return types is Residual, which maps back from a residual to canonical Try type (that one-to-one correspondence), allowing you to map over success type (e.g. Result<T, E> -> Result<U, E>). This is the axis that try_* functionality would potentially be generic over, e.g. collecting impl Iterator<Result<T, E>> -> Result<impl FromIterator<T>, E>.

And Residual does result in incidental complexity in expressing that genericism; what you want to say for Q: Try is roughly just <Q as Try>::ButWithOutput<T>, but instead it needs to be spelled as <<Q as Try>::Residual as Residual<T>>::TryType.

Expressing maximally flexible monads in Rust is known to be a hard problem. And nearly all of that flexibility is actually used for individually reasonable applications.

I think I agree with boats here in that being generic in this way probably isn't necessary, and that try_* functionality can stick to being defined in terms of the canonical try type, Result, for much more legible signatures and little/no cost to functionality, just generality, since callers would need to reify between their custom Try type and Result.

As an actual example, see Iterator::try_reduce:

// today's plumbing
type RebindTryOutput<T, O> = <<T as Try>::Residual as Residual<O>>::TryType;
fn Iterator::try_reduce<F, R>(&mut self, f: F) -> RebindTryOutput<R, Option<R::Output>>
where
    F: FnMut(Self::Item, Self::Item) -> R,
    R: Try<Output = Self::Item>,
    R::Residual: Residual<Option<R::Output>>,
;

// "ideal" spelling of today's plumbing
fn Iterator::try_reduce<F, R>(&mut self, f: F) -> R::RebindTryOutput<Option<Self::Item>>
where
    F: FnMut(Self::Item, Self::Item) -> R,
    R: Try<Output = Self::Item>,
    R::Residual: Residual<Option<Self::Item>>,
;

// just Result
fn Iterator::try_reduce<F, E>(&mut self, f: F) -> Result<Option<Self::Item>, E>
where
    F: FnMut(Self::Item, Self::Item) -> Result<Self::Item, E>,
;

Some kind of similar functionality to Residual probably still needs to exist, though, to make try blocks somewhat functional without mandatory type hinting.

Footnotes

  1. Since ? is essentially language functionality while the library plumbing remains unstable, the behavior of ? could theoretically be changed over an edition to use a different trait which is implemented differently. But while technically possible, it's a horrible thing to change in practice.

In practice, how much usage of ? and Try appear to use non-Result types?

I'm working on a project since 1 years that fully use all features of Try trait v2 so I hope the FromResiduel feature will not go away... I try to work on the doc to release it one day. I guess it will be a good example for this thread.

Using Result in the try_* funcions means giving up on this part of the motivation from the RFC:

Using the "error" terminology is a poor fit for other potential implementations of the trait.

At least in the scope of these functions. In some cases, such as OnceCell::get_or_try_init, that's probably not terrible, since if failing to init the cell, would generally be considered an error. But for something like try_fold, you are more likely to have situations where breaking is actually a success, not on error.

Maybe we can use "exception" rather than error. As the try "triggering" would generally be the exceptional case and the common case would be continuing the straight-line code.

commented

(NOT A CONTRIBUTION)

The trait which would actually be appropriate for generic return types is Residual, which maps back from a residual to canonical Try type (that one-to-one correspondence), allowing you to map over success type (e.g. Result<T, E> -> Result<U, E>). This is the axis that try_* functionality would potentially be generic over, e.g. collecting impl Iterator<Result<T, E>> -> Result<impl FromIterator, E>.

I think this is the part that people really are concerned about. Frankly, I think Residual should be removed, and all of these. highly generic APIs that cannot be written without it should be removed as well.

I'm going to be very honest about what I think, because I think it is also the opinion of many other prominent community members who are not involved in the libs/lang team clique and have been critical on issues like this or on Twitter. It seems to me that the product design group is trapped in some sort of code golf death spiral where one side creates extremely generic convenience APIs (i.e. try_collect) and the other side proposes whole new axes of abstraction to hide some of that highly generic type signature behind a language feature (i.e. keyword generics), while most users do not have any pressing need for these APIs or abstractions, which are at best "nice to have" and cannot justify the huge cognitive overhead they bring to the language and standard library.

The Try feature should be brought back to its original scope: making try and ? work with multiple types. The actual content of the RFC for Try v2 is fine, in my opinion, with the exception of the one little change I proposed in my previous comment. The addition of the Residual trait and the fake-monad type hacking by way of double projection from Try to Residual and back to Try with a swapped enclosed type needs to be dropped.

the fake-monad type hacking by way of double projection

yeah, I'd rather have proper functor, monad interfaces in Rust than such hackery (and I'd really appreciate that solely because dealing with ASTs which are extended/modified through various stages of progressing lead to that sort-of naturally, and being unable to abstract over multiple of them makes it harder than necessary to write generic AST mangling libraries imo).

Out of curiosity, @withoutboats, modulo exact naming,

Would you consider the use of monadic reprojection be more palatable if it were spelled -> R::Rebind<O> instead of the double projection (-> <R::Residual as Residual<O>>::TryType)? Presuming that it was just an alias to the use of Residual, that it showed up in docs as the alias, and that the language feature exists to associate a type alias to a trait without letting it be overridden.

What if it was a typical generic associated type of the Try trait instead of the double projection, but there was an associated trait alias used to bound the generic type1?

I ask because I'm legitimately interested in whether it's the use of a generic monadic rebind at all (given that it's not strictly necessary when alternative Try types can temporarily reify into Result) which is considered unnecessary cognitive overhead, or if it's just the use of a double projection to accomplish it. (The former is the inherent complexity of making the API generic over this axis; the latter is incidental complexity of how the result is achieved.)

I ask this now prompted by learning of an open draft experiment utilizing a similar double projection scheme to permit allocator-generic collections to do -> A::ErrorHandling::Result<T, E> to select between -> Result<T, E> and -> T based on whether the allocator is (statically) in fallible or infallible mode. This permits the unification of the e.g. reserve/try_reserve split, but at the cost of any potentially allocating method which would have had a try_* version having the more complex signature.


I do agree that doubled type projection is essentially unprecedented in stable std function signatures. The use of a type alias for clarity in the definition of functions doing so is telling, and at an absolute minimum imo the alias should be made public so it will show up in the API docs.

I doubted it for a moment, but the use of a <Type as Trait>::Type return type in the API docs of stable functions is precedented, at least. Namely, Option::as_deref is documented as having the signature (&self) -> Option<&<T as Deref>::Target> where T: Deref2.

Footnotes

  1. The ability to bound this is why we currently have <R::Residual as Residual<O>>::TryType and not R::Residual::TryType<O>. (...And that doing so would currently also cause an ambiguous associated type error, but IIUC that's considered a bug/fixable limitation.) The bound is necessary to support e.g. ErrnoResult.

  2. This is despite it being implemented with -> Option<&T::Target>; I presume rustdoc doesn't bother figuring out whether a projection can be treated as unambiguous. Especially since when such a type projection would be ambiguous in rustdoc is a complicated and distinct question than it being unambiguous in the source code.

commented

(NOT A CONTRIBUTION)

Would you consider the use of monadic reprojection be more palatable if it were spelled -> R::Rebind<O> instead of the double projection

Both the action of rebinding the wrapped type and the use of double projection to do so add cognitive overhead to the API. A GAT certainly seems simpler than a double projection, though when you start talking about "associated trait aliases" I get very suspicious. I think either aspect of this alone is complex enough to warrant a lot of caution, and require very compelling motivation. I don't see any of the enabled APIs as particularly well motivated, for exactly the reason we've discussed - you can just map things to Result if you really want, though even with Result I doubt the utility of all these try_ variants at all.

I ask this now prompted by learning of an open #111970 utilizing a similar double projection scheme to permit allocator-generic collections

My immediate impression is also to be dubious of these fallible allocator APIs. I think allocators is another area where the working group seems to have gone down a rabbit hole and lost touch with how much complexity they're adding to the APIs. I realize there are motivations, but these motivations need to be balanced against how difficult they make std to understand. Maybe I underestimate how compelling the motivations are for that case, I haven't followed the work closely.

Overall, I think most people who work on Rust open source are very disconnected from how most non-hobbyist users experience Rust. They don't have time to nerd out about the language, so the model of the type system in their head is a bit wrong or incomplete, and they can easily be frustrated or confused or led astray by the kinds of type wizardry the online Rust community embraces without question. Rust has always been trying to bring a great type system "to the masses" (in total opposition to Rob Pike's statement that users "are not capable of understanding a brilliant language"), but this means recognizing when the cognitive overhead complex types are adding is not justified by the utility they bring to the API. This is especially true when that utility is basically just omitting a type conversion, as in a lot of these Try cases.

Of course, this sort of thing is fine in third party crates, where it can be proved out and experimented with and in libraries targeted at a certain kind of user can be effective for that user in achieving their goals. But the standard library should be a bulwark against this sort of programming, because the standard library is the lowest common denominator for all users.

Namely, Option::as_deref is documented as having the signature (&self) -> Option<&<T as Deref>::Target> where T: Deref

This seems like sloppiness to me, as_deref should be in a separate impl block with the where clause on the impl block, rather than the where clause on the method in a normal impl block. This should solve the problem with rustdoc (though I also think this is a poor showing from rustdoc as well). Not sure if this is a technically breaking change to make, though.

I don't have too much to say regarding the actual design of the trait, although I think it's apt to bring up this argument I made in the original RFC: rust-lang/rfcs#3058 (comment)

The term "residual", despite being weird English, is unique enough that it could become synonymous with what it's being used for here. There are lots of terms that aren't used in Normal English that are used in Computer English (for example, verbose) and I think that adding another, although weird, is not the worst.

Essentially, however weird the idea of a "residual" is, the term is unique enough that its current meaning in Rust can be learned in isolation.

Folks have demonstrated that they have made use of the FromResidual trait in their own implementations and I think that it would be nice to retain this in the stable implementation. I do however sympathise greatly with the bounds required for methods that do use the Try trait, requiring both Try and FromResidual in the bounds.

I think that FromResidual should probably exist as a relatively "niche" feature that does not need to be understood to use the Try trait in most cases. It should be required to implement the Try trait, but that's a higher bar IMHO, and most people just want to make try_* methods that work without having to understand the whole system.

I feel like maybe more effort should be put into ensuring that the Try trait is easy to use, without sacrificing the flexibility of FromResidual.

@CAD97 just to note: I consider such double projection, especially as in applications of them also trait bounds on them can appear in downstream or implementation code, to be an implementation detail that imo shouldn't be necessary, i.e. should be abstracted by the language, leading to a cleaner interface.

re: the concern of "niche" or

Overall, I think most people who work on Rust open source are very disconnected from how most non-hobbyist users experience Rust. They don't have time to nerd out about the language, so the model of the type system in their head is a bit wrong or incomplete, and they can easily be frustrated or confused or led astray by the kinds of type wizardry the online Rust community embraces without question.

I believe that it shouldn't be necessary for most normal users to deal with the intricate parts of the API directly at all in most use cases, and if they do, it should be minimally invasive. A long type signature with a large amount of trait bounds and such that can't be abstracted away (except via macros, which makes the docs unhelpful) is a strong anti-pattern, and imo thus warrants a more fundamental solution, e.g. making all monads easier to express in rust instead of abstracting over it via a combination of multiple interlocked traits with potentially confusing semantics, and also potentially harder to read error messages in case of failures of type inference and such.

Such interfaces are not only annoying to write/copy-paste and debug, they pose a mental burden, make it harder to present users with good error messages (because the compiler doesn't see the actual abstraction but a workaround around the lack of abstraction, mostly), and might also make type inference/checking unnecessarily harder (and e.g. "simple guesses" by the compiler in case of errors also get much harder, both from the "implement this in the compiler" and "make it fast and maintainable" (also in regard to similar patterns which might evolve in third-party crates, etc.).

e.g. (rust-like pseudo-code)
enum ControlFlow<B, C> {
    /// Exit the operation without running subsequent phases.
    Break(B),
    /// Move on to the next phase of the operation as normal.
    Continue(C),
}

monad_morph<B, C> ControlFlow<B, C> {
    type Monadic<C> = ControlFlow<B, C>;
    fn pure<C>(t: C) -> Self::Monadic<C> {
        ControlFlow::Continue(t)
    }
    fn flat_map<C1, C2, F>(input: Self::Monadic<C1>, f: F) -> Self::Monadic<C2>
    where F: /* function trait used might vary per monad_morph */ FnOnce(C1) -> Self::Monadic<C2>,
    {
        match input {
            ControlFlow::Break(b) => ControlFlow::Break(b),
            ControlFlow::Continue(c) => f(c),
        }
    }
    /* a question that remains here would be how to handle the "short-circuiting" generally and effectively (that is, without many nested closures)
     * probably similar to the residual stuff, but it would be interesting to see how it could be done generally
     * (e.g. simple ControlFlow), while also allowing lazier interfaces (like monads similar to iterators (considering `flat_map` there)) */
}

also, another idea would be to introduce some kind of "tagged ControlFlow", and just use that everywhere (especially try_ functions), forgoing the most difficult parts of this, while still allowing guided interconversions (mediated by some kind of type-level tags (ZSTs))

commented

I think that FromResidual should probably exist as a relatively "niche" feature that does not need to be understood to use the Try trait in most cases.

That is already kinda the case. If you're using a Try type, you don't need to care about FromResidual. If you're writing one, then you will be forced to write exactly one impl of it, which is only fair.

Then again, if you're using one of the ready-made functions, you won't need to care much about the traits beyond the documentation.


However, unless I'm reading the thread wrong, most of the discussion right now seems to be about the Residual trait, as opposed to the Try & FromResidual traits. The relevant tracking issue is #91285, not here.

For what it's worth, I think things like try_collect are useful (and will be even more so when try trait stabilises and Result isn't the only result-like type anymore). That said, double projection does make all the relevant type signatures headache-inducing. So maybe something like this would work better?

trait TryWith<O>: Try {
    type TryWith: Try<Output = O, Residual = Self::Residual>;
}

I'd be cautious of GATs here, since they'd forbid any impl blocks from adding additional bounds on the projected Output, though I haven't thought about it much so I'll leave it at that.

But again, wrong tracking issue.


Also I'll echo @withoutboats on removing the default for R from FromResidual<R> for readability reasons. Appart from that I think the try trait design (which does not include the Residual trait) is fine.

As somebody who would like to see this stabilized, what can I do to help? Is there anything I can do to help push this forward?