Rust: Tracking issue for `ops::Try` (`try_trait` feature)

Created on 31 May 2017  ·  99Comments  ·  Source: rust-lang/rust

The Try trait from https://github.com/rust-lang/rfcs/pull/1859; implemented in PR https://github.com/rust-lang/rust/pull/42275.

Split off from https://github.com/rust-lang/rust/issues/31436 for clarity (per https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)

  • [ ] Stabilizing this will allow people to implement Iterator::try_fold

    • [ ] As part of stabilizing, re-open PR #62606 to document implementing try_fold for iterators

    • [ ] 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.)

A-error-handling B-RFC-implemented B-unstable C-tracking-issue Libs-Tracked T-lang T-libs

Most helpful comment

What's the current status of this feature?

All 99 comments

A couple of pieces of bikeshedding:

  • Do we have a particular motivation to call the associated type Error instead of Err? Calling it Err would make it line up with Result: the other one is already called Ok.

  • Do we have a particular motivation for having separate from_error and from_ok methods, instead of a single from_result which would be more symmetric with into_result which is the other half of the trait?

(updated version of playground link from the RFC)

@glaebhoerl

  • Error vs Err was discussed related to TryFrom in https://github.com/rust-lang/rust/issues/33417#issuecomment-269108968 and https://github.com/rust-lang/rust/pull/40281; I assume the name was chosen here for similar reasons.
  • I believe they're separate because they have different uses, and I expect it to be rare that someone actually has a Result that they're trying to turn into a T:Try. I prefer Try::from_ok and Try::from_error to always calling Try::from_result(Ok( and Try::from_result(Err(, and I'm happy to just impl the two methods over writing out the match. Perhaps that's because I think of into_result not as Into<Result>, but as "was it pass or fail?", with the specific type being Result as an unimportant implementation detail. (I don't want to suggest or reopen the "there should be a new type for produce-value vs early-return, though.) And for documentation, I like that from_error talks about ? (or eventually throw), while from_ok talks about success-wrapping (#41414), rather than having them both in the same method.

I'm not sure if this is the correct forum for this comment, please redirect me if it's not :smiley:. Perhaps this should have been on https://github.com/rust-lang/rfcs/pull/1859 and I missed the comment period; oops!


I was wondering if there is a case for splitting up the Try trait or removing the into_result method; to me currently Try is somewhat like the sum of traits FromResult (containing from_error and from_ok) and IntoResult (containing into_result).

FromResult enables highly ergonomic early exit with the ? operator, which I think is the killer use case for the feature. I think IntoResult can already be implemented neatly with per-use-case methods or as Into<Result>; am I missing some useful examples?

Following the Try trait RFC, expr? could desugar as:

match expr { // Removed `Try::into_result()` here.
    Ok(v) => v,
    Err(e) => return Try::from_error(From::from(e)),
}

The motivating examples I considered are Future and Option.

Future

We can implement FromResult naturally for struct FutureResult: Future as:

impl<T, E> FromResult for FutureResult {
    type Ok = T;
    type Error = E;
    fn from_error(v: Self::Error) -> Self {
        future::err(v)
    }
    fn from_ok(v: Self::Ok) -> Self {
        future::ok(v)
    }
}

Assuming a reasonable implementation for ? that will return from an impl Future valued function when applied to a Result::Err then I can write:

fn async_stuff() -> impl Future<V, E> {
    let t = fetch_t();
    t.and_then(|t_val| {
        let u: Result<U, E> = calc(t_val);
        async_2(u?)
    })
}
fn fetch_t() -> impl Future<T, E> {}
fn calc(t: T) -> Result<U,E> {}

That is exactly what I was trying to implement earlier today and Try nails it! But if we tried to implement the current Try for Future there is perhaps no canonical choice for into_result; it could be useful to panic, block, or poll once, but none of these seems universally useful. If there were no into_result on Try I can implement early exit as above, and if I need to convert a Future to a Result (and thence to any Try) I can convert it with a suitable method (call the wait method to block, call some poll_once() -> Result<T,E>, etc.).

Option

Option is a similar case. We implement from_ok, from_err naturally as in the Try trait RFC, and could convert Option<T> to Result<T, Missing> simply with o.ok_or(Missing) or a convenience method ok_or_missing.


Hope that helps!

I'm maybe very late to all this, but it occured to me this weekend that ? has a rather natural semantic in cases where you'd like to short-circuit on _success_.

fn fun() -> SearchResult<Socks> {
    search_drawer()?;
    search_wardrobe()
}

But in this case, the naming of the Try traits methods don't fit.

It would widen the meaning of the ? operator, though.

In prototyping a try_fold for rayon, I found myself wanting something like Try::is_error(&self) for the Consumer::full() methods. (Rayon has this because consumers are basically push-style iterators, but we still want to short-circuit errors.) I instead had to store the intermediate values as Result<T::Ok, T::Err> so I could call Result::is_err().

From there I also wished for a Try::from_result to get back to T at the end, but a match mapping to T::from_ok and T::from_error isn't _too_ bad.

The Try trait can provide a from_result method for ergonomics if from_ok and from_error are required. Or vice versa. From the examples given I see a case for offering both.

Since PR #42275 has been merged, does that mean this issue has been resolved?

@skade Note that a type can define whichever as the short-circuiting one, like LoopState does for some Iterator internals. Perhaps that would be more natural if Try talked about "continuing or breaking" instead of success or failure.

@cuviper The version I ended up needing was either the Ok type or the Error-in-original type. I'm hoping that destructure-and-rebuild is a general enough thing that it'll optimize well and a bunch of special methods on the trait won't be needed.

@ErichDonGubler This is a tracking issue, so isn't resolved until the corresponding code is in stable.

Experience report:

I've been a little frustrated trying to put this trait into practice. Several times now I've been tempted to define my own variant on Result for whatever reason, but each time I've wound up just using Result in the end, mostly because implementing Try was too annoying. I'm not entirely sure if this is a good thing or not, though!

Example:

In the new solver for the Chalk VM, I wanted to have an enum that indicates the result of solving a "strand". This had four possibilities:

enum StrandFail<T> {
    Success(T),
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

I wanted ?, when applied to this enum, to unwrap "success" but propagated all other failures upward. However, in order to implement the Try trait, I would have had to define a kind of "residual" type that encapsulates just the error cases:

enum StrandFail {
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

But once I've got this type, then I might as well make StrandResult<T> an alias:

type StrandResult<T> = Result<T, StrandFail>;

and this is what I did.

Now, this doesn't necessarily seem worse than having a single enum -- but it does feel a bit odd. Usually when I write the documentation for a function, for example, I don't "group" the results by "ok" and "error", but rather talk about the various possibilities as "equals". For example:

    /// Invoked when a strand represents an **answer**. This means
    /// that the strand has no subgoals left. There are two possibilities:
    ///
    /// - the strand may represent an answer we have already found; in
    ///   that case, we can return `StrandFail::NoSolution`, as this
    ///   strand led nowhere of interest.
    /// - the strand may represent a new answer, in which case it is
    ///   added to the table and `Ok` is returned.

Note that I didn't say "we return Err(StrandFail::NoSolution). This is because the Err just feels like this annoying artifact I have to add.

(On the other hand, the current definition would help readers to know what the behavior of ? is without consulting the Try impl.)

I guess this outcome is not that surprising: the current Try impl forces you to use ? on things that are isomorphic to Result. This uniformity is no accident, but as a result, it makes it annoying to use ? with things that are not basically "just Result". (For that matter, it's basically the same problem that gives rise to NoneError -- the need to artificially define a type to represent the "failure" of an Option.)

I'd also note that, in the case of StrandFail, I don't particularly want the From::from conversion that ordinary results get, though it's not hurting me.

Is the associated Try::Error type ever exposed to / used by the user directly? Or is it just needed as part of the desugaring of ?? If the latter, I don't see any real problem with just defining it "structurally" - type Error = Option<Option<(Strand, Minimums)>> or whatever. (Having to figure out the structural equivalent of the "failure half" of the enum definition isn't great, but seems less annoying than having to rejigger the whole public-facing API.)

I don't follow either. I have successfully implemented Try for a type that had an internal error representation and it felt natural having Ok and Error being the same type. You can see the implementation here: https://github.com/kevincox/ecl/blob/8ca7ad2bc4775c5cfc8eb9f4309b2666e5163e02/src/lib.rs#L298-L308

In fact is seems like there is a fairly simple pattern for making this work.

impl std::ops::Try for Value {
    type Ok = Self;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { v }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        if self.is_ok() { Ok(val) } else { Err(val) }
    }
}

If you want to unwrap the success something like this should work:

impl std::ops::Try for StrandFail<T> {
    type Ok = T;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { StrandFail::Success(v) }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        match self {
            StrandFail::Success(v) => Ok(v),
            other => Err(other),
        }
    }
}

Defining a structural type is possible but feels pretty annoying. I agree I could use Self. It feels a bit wacky to me though that you can then use ? to convert from StrandResult to Result<_, StrandResult> etc.

Great experience report, @nikomatsakis! I've also been dissatisfied with Try (from the other direction).

TL/DR: I think FoldWhile got this right, and we should double-down on the Break-vs-Continue interpretation of ? instead talking about it in terms of errors.

Longer:

I keep using Err for something closer to "success" than "error" because ? is so convenient.

So if nothing else, I think the description I wrote for Try is wrong, and shouldn't talk about a "success/failure dichotomy", but rather abstract away from "errors".

The other thing I've been thinking about is that we should consider some slightly-crazy impls for Try. For example, Ordering: Try<Ok = (), Error = GreaterOrLess>, combined with "try functions", could allow cmp for struct Foo<T, U> { a: T, b: U } to just be

fn cmp(&self, other: &self) -> Ordering try {
    self.a.cmp(&other.a)?;
    self.b.cmp(&other.b)?;
}

I don't yet know whether that's the good kind of crazy or the bad kind :laughing: It's certainly got some elegance to it, though, since once things are different you know they're different. And trying to assign "success" or "error" to either side of that doesn't seem to fit well.

I do also note that that's another instance where the "error-conversion" isn't helpful. I also never used it in the "I want ? but it's not an error" examples above. And it's certainly known to cause inference sadness, so I wonder if it's a thing that should get limited to just Result (or potentially removed in favour of .map_err(Into::into), but that's probably infeasible).

Edit: Oh, and all that makes me wonder if perhaps the answer to "I keep using Result for my errors instead of implementing Try for a type of my own" is "good, that's expected".

Edit 2: This is not unlike https://github.com/rust-lang/rust/issues/42327#issuecomment-318923393 above

Edit 3: Seems like a Continue variant was also proposed in https://github.com/rust-lang/rfcs/pull/1859#issuecomment-273985250

My two cents:

I like @fluffysquirrels's suggestion of splitting the trait into two traits. One for converting to a result, and another to convert from a result. But I do think we should keep the into_result or equivalent as part of the desugaring. And I think at this point we have to since using Option as a Try has stabilized.

I also like @scottmcm's idea of using names that suggest break/continue rather than error/ok.

To put the specific code in here, I like how this reads:

https://github.com/rust-lang/rust/blob/ab8b961677ac5c74762dcea955aa0ff4d7fe4915/src/libcore/iter/iterator.rs#L1738-L1746

It's of course not quite as nice as a straight loop, but with "try methods" it would be close:

self.try_for_each(move |x| try { 
    if predicate(&x) { return LoopState::Break(x) } 
}).break_value() 

For comparison, I find the "error vocabulary" version really misleading:

self.try_for_each(move |x| { 
    if predicate(&x) { Err(x) } 
    else { Ok(()) } 
}).err() 

Can we implement Display for NoneError? It would allow the failure crate to automatically derive From<NoneError> for failure::Error. See https://github.com/rust-lang-nursery/failure/issues/61
It should be a 3 line change, but I'm not sure about the process of RFCs and such.

@cowang4 I'd like to try to avoid enabling any more mixing of Result-and-Option right now, as the type is unstable mostly to keep our options open there. I wouldn't be surprised if we ended up changing the design of Try and the desugar into something that didn't need NoneError...

@scottmcm Okay. I see your point. I'd eventually like a clean way to sugar the pattern of returning Err when an library function returns None. Maybe you know of one other than Try? Example:

fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    if let Some(filestem) = pb.file_stem() {
        if let Some(filestr) = filestem.to_str() {
            return Ok(MyStruct {
                filename: filestr.to_string()
            });
        }
     }
    Err(_)
}

Once I found this experimental feature and the failure crate, I naturally gravitated to:

use failure::Error;
fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    Ok({
        title: pb.file_stem?.to_str()?.to_string()
    })
}

Which _almost_ works, except for the lack of a impl Display for NoneError like I mentioned before.
But, if this isn't the syntax we'd like to go with, then maybe there could be another function / macro that simplifies the pattern:

if option.is_none() {
    return Err(_);
}

@cowang4 I believe that would work if you implemented From<NoneError> for failure::Error, which used your own type that implemented Display.

However, it is probably better practice to use opt.ok_or(_)? so you can explicitly say what the error should be if the Option is None. In your example, for instance, you may want a different error if pb.file_stem is None than if to_str() returns None.

@tmccombs I tried creating my own error type, but I must've done it wrong. It was like this:

#[macro_use] extern crate failure_derive;

#[derive(Fail, Debug)]
#[fail(display = "An error occurred.")]
struct SiteError;

impl From<std::option::NoneError> for SiteError {
    fn from(_err: std::option::NoneError) -> Self {
        SiteError
    }
}

fn build_piece(cur_dir: &PathBuf, piece: &PathBuf) -> Result<Piece, SiteError> {
    let title: String = piece
        .file_stem()?
        .to_str()?
        .to_string();
    Ok(Piece {
        title: title,
        url: piece
            .strip_prefix(cur_dir)?
            .to_str()
            .ok_or(err_msg("tostr"))?
            .to_string(),
    })
}

And then I tried using my error type...

error[E0277]: the trait bound `SiteError: std::convert::From<std::path::StripPrefixError>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
    | |___________________________________^ the trait `std::convert::From<std::path::StripPrefixError>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

error[E0277]: the trait bound `SiteError: std::convert::From<failure::Error>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
197 | |             .to_str()
198 | |             .ok_or(err_msg("tostr"))?
    | |_____________________________________^ the trait `std::convert::From<failure::Error>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

Okay, I just realized that it wants to know how to convert from other error types to my error, which I can write generically:

impl<E: failure::Fail> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

Nope...

error[E0119]: conflicting implementations of trait `std::convert::From<SiteError>` for type `SiteError`:
   --> src/main.rs:183:1
    |
183 | impl<E: failure::Fail> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: conflicting implementation in crate `core`:
            - impl<T> std::convert::From<T> for T;

Okay, what about std::error::Error?

impl<E: std::error::Error> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

That doesn't work either. Partly because it conflicts with my From<NoneError>

error[E0119]: conflicting implementations of trait `std::convert::From<std::option::NoneError>` for type `SiteError`:
   --> src/main.rs:181:1
    |
175 | impl From<std::option::NoneError> for SiteError {
    | ----------------------------------------------- first implementation here
...
181 | impl<E: std::error::Error> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `SiteError`
    |
    = note: upstream crates may add new impl of trait `std::error::Error` for type `std::option::NoneError` in future versions

Which is weird because I thought that NoneError didn't implement std::error::Error. When I comment out my non-generic impl From<NoneError> I get:

error[E0277]: the trait bound `std::option::NoneError: std::error::Error` is not satisfied
   --> src/main.rs:189:25
    |
189 |       let title: String = piece
    |  _________________________^
190 | |         .file_stem()?
    | |_____________________^ the trait `std::error::Error` is not implemented for `std::option::NoneError`
    |
    = note: required because of the requirements on the impl of `std::convert::From<std::option::NoneError>` for `SiteError`
    = note: required by `std::convert::From::from`

Do I have to write all the Froms manually. I though that the failure crate was supposed to derive them?

Maybe I should stick with option.ok_or()

Do I have to write all the Froms manually. I though that the failure crate was supposed to derive them?

I don't think the failure crate does that. But I could be wrong.

Ok, so I re-examined the failure crate, and if I'm reading the documentation and different versions right, it's designed to always use the failure::Error as the Error type in your Results, see here. And, it does implement a blanket impl Fail trait for most Error types:

impl<E: StdError + Send + Sync + 'static> Fail for E {}

https://github.com/rust-lang-nursery/failure/blob/master/failure-1.X/src/lib.rs#L218

And then a impl From so that it can Try/? other errors (like ones from std) into the overarching failure::Error type.
rust impl<F: Fail> From<F> for ErrorImpl
https://github.com/rust-lang-nursery/failure/blob/d60e750fa0165e9c5779454f47a6ce5b3aa426a3/failure-1.X/src/error/error_impl.rs#L16

But, it's just that since the error relevant to this rust issue, NoneError, is experimental, it can't be converted automatically yet, because it doesn't implement the Display trait. And, we don't want it to, because that blurs the line between Options and Results more. It'll all probably get re-worked and sorted out eventually, but for now, I'll stick to the de-sugared techniques that I've learned.

Thank you everyone for helping. I'm slowly learning Rust! :smile:

I'll stick to the de-sugared techniques that I've learned.

:+1: Honestly, I think .ok_or(...)? will remain the way to go (or .ok_or_else(|| ...)?, of course). Even if NoneError _had_ a Display impl, what would it say? "Something wasn't there"? That's not a great error...

Attempting to move closer to a concrete proposal...

I'm starting to like the TrySuccess alternative. Interestingly, I think _nobody_, myself included, liked that one originally -- it's not even in the final version of the RFC. But thankfully it lives on in the history: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#using-an-associated-type-for-the-success-value

It feels to me like the biggest objection to it was the reasonable complaint that a whole core trait for just an associated type (trait TrySuccess { type Success; }) was overkill. But with catchtry blocks back (https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777) to doing ok-wrapping (as in the RFC), all of a sudden it has a direct and important use: this is the trait that controls that wrapping. Coupled with the "? should always produce the same type" goal that I think was generally desired, the trait seems to better hold its weight. Strawman:

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

That associated type, as the one that will be returned from the ? operator, is also the one that clearly needs to exist. That means it doesn't hit the "had to define a kind of 'residual' type" annoyance that Niko articulated. And it being () is reasonable, even common, so avoids NoneError-like contortions.

Edit: For the bikeshed, "return" might be a good word, since this is what happens when you return from a try method (aka ok-wrapping). return is also the monad operator name for this, iiuc...

Edit 2: My thoughts on the other trait(s)/method(s) haven't stabilized yet, @tmccombs.

@scottmcm just to be clear, or you suggesting the following two traits?

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

trait Try<E>: TryContinue {
  fn try(self) -> Result<Self::Continue, E>
}

And desuguring x? would look something like:

x.try() match {
    Ok(c) => c,
   Err(e) => throw e // throw here is just a placeholder for either returning or breaking out of a try block
}

and try { ...; expr} would desugar to something like:

{ 
    ...
    TryContinue::from_continue(expr);
}

@scottmcm I too find that variant much more appealing when considering ok-wrapping =)

Continuing on to the next trait, I think the one for ? becomes this (modulo massive bikeshedding):

trait Try<Other: TryContinue = Self>: TryContinue + Sized {
    fn check(x: Other) -> ControlFlow<Other::Continue, Self>;
}

enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

Assorted justification:

  • This requires TryContinue for its argument type so that the type of an x? expression is always the same
  • This defaults the type parameter to Self for simple cases like try_fold that inspect and return the same type, so are fine with just T: Try.
  • This trait extends TryContinue because if a type used as a return type allows ? in its body, then it should also support ok-wrapping.
  • The new enum is isomorphic to result, but avoids talking in terms of "errors" as discussed above, and as a result I think it provides a very clear meaning for each of the variants.
  • The argument to ? is the generic type so that, like TryContinue, it "produces a Self from something"

Full proof-of-concept demo, including macros for try{}/? and impls for Option, Result, and Ordering: https://play.rust-lang.org/?gist=18663b73b6f35870d20fd172643a4f96&version=stable (thanks @nikomatsakis for making the original one of these a year ago 🙂)

Downsides:

  • Still has the phantom type parameters in the Result impl that I don't really like

    • But maybe that's a good tradeoff for not needing an extra LessOrGreater type in the Ordering impl.

    • And phantom type parameters in impls are less icky than in types anyway

  • Doesn't give a consistent From transform

    • But that might be good, as StrandResult didn't care anyway, for something something like SearchResult it would be weird as the Break path is the success path, and it might end up helping inference in the nested-? cases anyway.

  • It's not as obvious what throw syntax would be

    • Which loses the explanation of ? as Ok(x) => x, Err(r) => throw e.into() that I really liked

    • But also could let throw; be the way to produce None (via something like impl<T> Throw<()> for Option<T>), which is way better than throw NoneError;

    • And separating that out might be good anyway, since throw LessOrGreater::Less would have been _really_ silly.

Edit: Ping @glaebhoerl, whose opinion I'd like on this as a big participant in RFC 1859.

Edit 2: Also cc @colin-kiegel, for this statement from https://github.com/rust-lang/rfcs/pull/1859#issuecomment-287402652:

I wonder if the essentialist approach could adopt some of the elegance of the reductionists without sacrificing the goals above.

I really like that proposal.

It's not as obvious what throw syntax would be

Ignoring the baggage of the throw keyword from other languages, "throw" does sort of make sense, in that you are "throwing" the value up to a higher scope. Python and scala also do use exceptions for control flow other than error cases (though, in the case of scala you wouldn't usually use try/catch directly), so there is some precedent for using throw for successful paths.

@tmccombs

Ignoring the baggage of the throw keyword from other languages, "throw" does sort of make sense, in that you are "throwing" the value up to a higher scope.

Though it comes with similar baggage, I suspect "raise" is a better fit:

throw (v): to put or cause to go or come into some place, position, condition, etc., as if by hurling:

raise (v): to move to a higher position; lift up; elevate

There may be a way to combine "raise" with the logic of ?, given that raise can also mean "collect". Something like: Ok(v) => v, Err(e) => raise From::from(e), where raise mimics the matched pattern (e.g. given a pattern Err(e) it is syntactic magic for ControlFlow::Break(Err(...))).

I suspect "raise" is a better fit

good point

@scottmcm

It's not as obvious what throw syntax would be

Is there a reason we can't have from_err(value: Other) ?

UPDATE: Maybe I'm confused about the role of Other, actually. I have to study this trait a bit more. =)

@nikomatsakis

Is there a reason we can't have from_err(value: Other)?

Well, Other is a whole ?-able type (like a Result), so I wouldn't want throw Ok(4) to work. (And it need to be the whole disjunction to avoid forcing the introduction of an artificial error type.) For example, I think our current Option-Result interop would be equivalent to this (and the inverse):

impl<T, F, U> Try<Option<U>> for Result<T, F>
   where F: From<NoneError>
{
    fn check(x: Option<U>) -> ControlFlow<U, Self> {
        match x {
            Some(x) => ControlFlow::Continue(x),
            None => ControlFlow::Break(Err(From::from(NoneError))),
        }
    }
}

My current inclination for throw would be for this:

trait TryBreak<T> : TryContinue { from_break(_: T) -> Self }

with

throw $expr  ==>  break 'closest_catch TryBreak::from_break($expr)
  • Extends TryContinue so that ok-wrapping is also available for the return type of the method in which it's used
  • Not an associated type so you could impl both, say, TryBreak<TryFromIntError> and TryBreak<io::Error> for a type if you wanted.

    • and impl<T> TryBreak<()> for Option<T> to enable just throw; feels plausible in a way that an associated error type of () didn't.

    • impl<T> TryBreak<!> for T would be nice too, but probably is incoherent.

(Aside: these trait and method names are terrible; please help!)

My thoughts about the other issues raised here haven't gelled into an easily articulable form yet, but with respect to the type inference problems around the From::from() desugaring (I can't remember if this was also discussed somewhere else recently, or only here?):

The instinctual sense (from Haskell experience) that "that way lie type inference ambiguities" was one of (though not the main) reasons why I didn't want to have a From conversion as part of the original RFC. Now that it's baked into the cake, though, I wonder if we couldn't try to have that cake and eat it too by 'just' adding a special defaulting rule for it to the type inferencing process?

That is, I think: whenever we see a From::from() [optionally: one that was inserted by the ? desugaring, rather than written manually], and we know exactly one of the two types (input vs. output), while the other one is ambiguous, we default the other to be the same as the one. In other words, I think, when it's not clear which impl From to use, we default to impl<T> From<T> for T. This is, I think, basically always what you actually want? It's maybe a bit ad-hoc, but if it does work, the benefits seem well worth the costs IMHO.

(I also thought From was already a lang item, precisely due to the ? desugaring, but it doesn't seem to be? In any case, it's already in some ways special for that reason.)

in @scottmcm 's proposal, From::from() is _not_ part of the desugaring, rather it is in the implementation of Try for Result.

@tmccombs I wasn't proposing an amendment to his proposal.

The try{} RFC (https://github.com/rust-lang/rfcs/pull/2388) has been discussing try blocks where one doesn't care about the result. This alternative seems to handle that decently well, as it can choose to ignore the error types entirely if it so wishes, allowing

let IgnoreErrors = try {
    error()?;
    none()?;
};

Proof-of-concept implementation using the same traits as before: https://play.rust-lang.org/?gist=e0f6677632e0a9941ed1a67ca9ae9c98&version=stable

I think there's some interesting possibilities there, especially since a custom implementation could, say, only take results and bound E: Debug so it automatically logs any error that happens. Or a version could be made intended specifically as a return type for main in conjunction with Termination that "just works" to let you use ? without a complex type signature (https://github.com/rust-lang/rfcs/issues/2367).

I've had similar issues as those evidenced by @nikomatsakis using the existing version of the Try trait. For the specific issues, see https://github.com/SergioBenitez/Rocket/issues/597#issuecomment-381533108.

The trait definitions proposed by @scottmcm resolve these issues. They seem to be more complicated than necessary, however. I took a crack at reimplementing them and came up with the following:

#[derive(Debug, Copy, Clone)]
enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

// Used by `try { }` expansions.
trait FromTry: Try {
    fn from_try(value: Self::Continue) -> Self;
}

// Used by `?`.
trait Try<T = Self>: Sized {
    type Continue;
    fn check(x: T) -> ControlFlow<Self::Continue, Self>;
}

The main change is that the Continue associated type is on the Try trait as opposed to the FromTry (previously TryContinue). Besides simplifying the definitions, this has the advantage that Try can be implemented independent of FromTry, which I posit to be more common, and that implementing FromTry is simplified once Try is implemented. (Note: if it's desired that Try and FromTry be implemented in unison, we can simply move the from_try method into Try)

See the complete playground with implementations for Result and Option as well as Rocket's Outcome at this playground.

Thanks for the report, @SergioBenitez! That implementation matches the "flip the type parameters" alternative version of the original trait proposal in RFC 1859: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#unresolved-questions

The biggest thing that loses is the property that typeof(x?) depends only on typeof(x). Lacking that property was one of the concerns with the original ("I worry a bit about readability in code along these lines" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967) and an advantage of the final reductionist proposal ("For any given type T, ? can produce exactly one kind of ok/error value" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310). Of course, there were also arguments that said property is unnecessary or too restrictive, but in the final summary before FCP it was still there as an advantage (https://github.com/rust-lang/rfcs/pull/1859#issuecomment-295878466).

Besides simplifying the definitions, this has the advantage that Try can be implemented independent of FromTry, which I posit to be more common

Certainly today from_ok is less common, as Try and do catch are unstable. And even if do catch were stable, I agree that it'll be used less than ? overall (since most such blocks contain multiple ?s).

From the perspective of the trait and its operations, however, I think "wrap a 'keep going' value in the carrier type" is an absolutely essential part of ?. One rarely goes through the trait for that today -- just using Ok(...) or Some(...) instead -- but it's critical for generic usage. For example, try_fold:

https://github.com/rust-lang/rust/blob/8728c7a726f3e8854f5a80b474d1a8bacab10304/src/libcore/iter/iterator.rs#L1478-L1482

If a function's going to return a carrier type, it's critical that there's a way to put the value of interest into that carrier type. And, importantly, I don't think providing this is a hardship. It has a very natural definition for Result, for Option, for Outcome, etc.

@Centril has also pointed out before that "wrap a scalar in a carrier" is also a simpler construct theoretically. For examples, Haskell calls it the Pointed typeclass, though I don't think we want to generalize it _that_ far in Rust: allowing try { 4 }vec![4] seems like overkill to me.

I'm also imagining a future where, like async functions are proposed to wrap the block value into a future, we might have try functions that wrap the block value into a fallible type. There, again, the TryContinue is the critical part -- such a function might not even use ?, if we got a throw construct.

So all that is, unfortunately, more philosophical than concrete. Let me know if it made any sense, or if you think the opposite in any of the parts :slightly_smiling_face:

Edit: Apologies if you got an email with an early version of this; I hit "comment" too early...

The biggest thing that loses is the property that typeof(x?) depends only on typeof(x).

Ah yes, absolutely. Thanks for pointing that out.

Of course, there were also arguments that said property is unnecessary or too restrictive, but in the final summary before FCP it was still there as an advantage (rust-lang/rfcs#1859 (comment)).

Are there any specific examples of where it might be too restrictive?

If a function's going to return a carrier type, it's critical that there's a way to put the value of interest into that carrier type. And, importantly, I don't think providing this is a hardship.

I think that's a fair analysis. I agree.

@SergioBenitez From https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967

So the question is, are the proposed restrictions enough? Are there good uses of error case context in determining the success type? Are there likely abuses?

I can say from my experience in futures that there may well be some useful cases here. In particular, the Poll type that you talk about can be processed in a couple ways. Sometimes, we want to jump out on the NotReady variant and be left with essentially a Result value. Sometimes, we're interested only in the Ready variant and want to jump out on either of the other variants (as in your sketch). If we allow the success type to be determined in part by the error type, it's more plausible to support both of these cases, and basically use the context to determine what kind of decomposition is desired.

OTOH, I worry a bit about readability in code along these lines. This feels qualitatively different than simply converting the error component -- it means that the basic match that ? would be performing is dependent on contextual information.

So one could imagine a trait that moved _both_ types to type parameters, like

trait Try<T,E> {
    fn question(self) -> Either<T, E>;
}

And use that do enable all of

let x: Result<_,_> = blah.poll()?; // early-return if NotReady
let x: Async<_> = blah.poll()?; // early-return if Error
let x: i32 = blah.poll()?; // early-return both NotReady and Errors

But I think that's definitely a bad idea, since it means that these don't work

println!("{}", z?);
z?.method();

Since there's no type context to say what to produce.

The other version would be to enable things like this:

fn foo() -> Result<(), Error> {
    // `x` is an Async<i32> because NotReady doesn't fit in Result
    let x = something_that_returns_poll()?;
}
fn bar() -> Poll<(), Error> {
    // `x` is just i32 because we're in a Poll-returning method
    let x = something_that_returns_poll()?;
}

My instinct there is that the inference "flowing out of the ?" there is too surprising, and thus this is in the "too clever" bucket.

Critically, I don't think that not having it is too restrictive. my_result? in a -> Poll function doesn't need it, as the success type is the same as usual (important to keep synchronous code working the same in async contexts) and the error variant converts fine too. Using ? on a Poll in a method that returns Result seems like an anti-pattern anyway, not something that should be common, so using (hypothetical) dedicated methods like .wait(): T (to block for the result) or .ready(): Option<T> (to check if it's done) to choose the mode is probably better anyway.

This is "interesting" https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug

I don't like these try (do catch) blocks, they don't seem very newcomer-friendly.

I'm trying to assemble the current state of the proposal, which seems spread across multiple comments. Is there a single summary of the currently proposed set of traits (which drop the Error associated type)?

Early on in this thread I see a comment about splitting Try into separate to/from error traits -- are there any plans to implement that split?

It would be useful to have transparent conversion from Result<T, E> to any type Other<T2, E> on question mark -- this would let existing IO functions be called with nice syntax from within a function with a more specialized (e.g. lazy) return type.

pub fn async_handler() -> AsyncResult<()> {
    let mut file = File::create("foo.txt")?;
    AsyncResult::lazy(move || {
        file.write_all(b"Hello, world!")?;
        AsyncResult::Ok(())
    })
}

Semantically this feels like From::from(E) -> Other<T2, E>, but use of From is currently restricted to the existing Result-equivalent Try implementation.

I really think NoneError should have a separate tracking issue. Even if Try trait never gets stabilized, NoneError should get stabilized because it makes using ? on Option much more ergonomic. Consider this for errors like struct MyCustomSemanthicalError; or errors implementing Default. None could easily be converted into MyCustomSeemanthicalError via From<NoneError>.

In working on https://github.com/rust-analyzer/rust-analyzer/, I have met a slightly different papercut from insufficiencies in the ? operator, particularly when the return type is Result<Option<T>, E>.

For this type, it it makes sense for ? to effectively desugar to:

match value? {
    None => return Ok(None),
    Some(it)=>it,
}

where value is of type Result<Option<V>, E>, or:

value?

where value is Result<V, E>. I believe that this is possible if the standard libraries implements Try in the following way for Option<T>, although I haven't explicitly tested this and I think there may be specialisation issues.

enum NoneError<E> {
    None,
    Err(E),
}

impl From<T> for NoneError<T> {
    fn from(v: T) -> NoneError<T> {
        NoneError:Err(v)
    }
}

impl<T, E> std::Ops::Try for Result<Option<T>, E> {
    type OK = T;
    type Error = NoneError<E>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            Ok(option) => {
                if let Some(inner) = option {
                    Ok(inner)
                } else {
                    Err(NoneError::None)
                }
            }
            Err(error) => {
                Err(NoneError::Err(error));
            }
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::Err(error) => Err(error),
            None => Some(None),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(Some(v))
    }
}

impl<T> std::Ops::Try for Option<T> {
    type OK = T;
    type Error = NoneError<!>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            None => Err(NoneError::None),
            Some(v) => Ok(v),
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::None => Some(None),
            _ => unreachable!("Value of type ! obtained"),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(v)
    }
}

When asking @matklad why he couldn't create a custom enum implementing Try, which would be called Cancellable in this case, he pointed out that std::ops::Try is unstable, so it can't be used anyway given that rust-analyzer (currently) targets stable rust.

Repost from https://github.com/rust-lang/rust/issues/31436#issuecomment-441408288 because I wanted to comment on this, but I think that was the wrong issue:

Essentially, a situation I have is callbacks in a GUI framework - instead of returning an Option or Result, they need to return a UpdateScreen, to tell the framework if the screen needs to be updated or not. Often I don't need logging at all (it's simply not practical to log on every minor error) and simply return a UpdateScreen::DontRedraw when an error has occurred. However, with the current ? operator, I have to write this all the time:

let thing = match fs::read(path) {
    Ok(o) => o,
    Err(_) => return UpdateScreen::DontRedraw,
};

Since I can't convert from a Result::Err into a UpdateScreen::DontRedraw via the Try operator, this gets very tedious - often I have simple lookups in hash maps that can fail (which isn't an error) - so often in one callback I have 5 - 10 usages of the ? operator. Because the above is very verbose to write, my current solution is to impl From<Result<T>> for UpdateScreen like this, and then use an inner function in the callback like this:

fn callback(data: &mut State) -> UpdateScreen {
     fn callback_inner(data: &mut State) -> Option<()> {
         let file_contents = fs::read_to_string(data.path).ok()?;
         data.loaded_file = Some(file_contents);
         Some(())
     }

    callback_inner(data).into()
}

Since the callback is used as a function pointer, I can't use an -> impl Into<UpdateScreen> (for some reason, returning an impl is currently not allowed for function pointers). So the only way for me to use the Try operator at all is to do the inner-function trick. It would be nice if I could simply do something like this:

impl<T> Try<Result<T>> for UpdateScreen {
    fn try(original: Result<T>) -> Try<T, UpdateScreen> {
        match original {
             Ok(o) => Try::DontReturn(o),
             Err(_) => Try::Return(UpdateScreen::DontRedraw),
        }
    }
}

fn callback(data: &mut State) -> UpdateScreen {
     // On any Result::Err, convert to an UpdateScreeen::DontRedraw and return
     let file_contents = fs::read_to_string(data.path)?;
     data.loaded_file = Some(file_contents);
     UpdateScreen::Redraw
}

I am not sure if this would be possible with the current proposal and just wanted to add my use-case for consideration. It would be great if a custom Try operator could support something like this.

@joshtriplett Sorry for taking a while to get back to this. I've put together a working prototype of the proposal at https://github.com/rust-lang/rust/compare/master...scottmcm:try-trait-v2 to be concrete. I hope to try out some more things with it.

@scottmcm Do you have some higher-level explanation of the changes?

@scottmcm FWIW I tried your changes in rayon too:
https://github.com/rayon-rs/rayon/compare/master...cuviper:try-trait-v2
(still using private copies rather than unstable items)

So what is the solution to convert the Option NoneError? It seems that, because, it implements the Try Trait, it'll not compile unless you enable using that particular (unstable?) feature.

So basically, you cannot use the ? operator with Option as far as I'm aware?

@omarabid, the operator is stable for use with Option or Result, but you can't use Try as a generic constraint until it's stable. It's perfectly fine to use ? on an Option in a function returning Option, as you don't have to involve NoneError at all. You can also return a Result if you erase types:

use std::fmt::Debug;

pub struct Error(Box<dyn Debug>);

impl<T: Debug + 'static> From<T> for Error {
    fn from(error: T) -> Self {
        Error(Box::new(error))
    }
}

pub fn try_get<T>(slice: &[T], index: usize) -> Result<&T, Error> {
    Ok(slice.get(index)?)
}

(playground)

@scottmcm, your prototype try-trait-v2 fails this example!

If we don't want my example to break, try-trait-v2 will need something like:

#[unstable(feature = "try_trait_v2", issue = "42327")]
impl<T, U, E: From<NoneError>> ops::Bubble<Result<U, E>> for Option<T> {
    fn bubble(self) -> ops::ControlFlow<T, Result<U, E>> {
        match self {
            Some(x) => ops::ControlFlow::Continue(x),
            None => ops::ControlFlow::Break(Err(E::from(NoneError))),
        }
    }
}

What's the current status of this feature?

PR #62606 to document implementing try_fold for iterators should be reopened once this becomes stable.

Edit: Updated the op with a tracking item for that ~ scottmcm

Are there any plans to replace the Try trait with any of the alternatives suggested in this thread? The version suggested by @scottmcm seems fine. I want to continue to use the ? operator with Option, and I think the Try trait should be changed to not force Result semantics on Option.

Using @scottmcm's alternative would allow us to use ? with Option and get rid of NoneError. I agree with @nikomatsakis (comment) that the Try trait shouldn't promote the need to "artificially define a type to represent the 'failure' of an Option".

pub struct Error(Box<dyn std::fmt::Debug>);
impl<T: std::fmt::Debug + 'static> From<T> for Error { fn from(error : T) -> Self { Error(Box::new(error)) } }
type Result<T> = std::result::Result<T, Error>;

Beginner here, I was a bit stubborn in wanting to get ? to automatically type erase both any Error and Option.
After spending way too much time trying to understand why other likely solutions cannot be implemented I've found @cuviper is the closest to what I can get.
Some explanations would have helped, but at least I got to get closely acquainted to some Rust metaprogramming limitations.
So I tried to figure it out and explain in concrete terms.
This thread seems the most likely crossroad where I can hopefully help out anyone like me stumbling on this, feel free to correct :

  • Using Debug (common to StdError and NoneError) instead of StdError :
    A generic From<T: StdError> for Error and a specialized From<NoneError> for Error conflicts
    Because it would be ambiguous if NoneError implemented StdError (apparently even default does not allow overrides and enforces exclusivity)
    This could be solved by a "negative bound" which is not supported, maybe because it would be the only use case ? (over specialization)
    impl StdError for NoneError can only be defined alongside StdError or NoneError itself, so that it is consistent across any downstream.
  • Error cannot be an alias :
    type Error = Box<Debug> binds Debug which makes From<T:Debug> for Error conflict with From<T> for T (reflexive From for idempotence)

So because Error cannot implement Debug you might want to have an helper to unwrap into a Result with transitive Debug :

fn from<T>(result : Result<T>) -> std::result::Result<T, Box<dyn std::fmt::Debug>> { match result { Ok(t) => Ok(t), Err(e) => Err(e.0) } }

It cannot be impl From<Result<T>> for StdResult<T> nor Into<StdResult> for Result<T> (cannot impl upstream trait for upstream type)

For example, you can use it to get a Debug return for Termination :

fn main() -> std::result::Result<(), Box<dyn std::fmt::Debug>> { from(run()) }

Evil idea : maybe the ? operator could somehow represent a monadic bind, so

let x: i32 = something?;
rest of function body

becomes

Monad::bind(something, fn(x) {
    rest of function body
})

A terrible idea, but it pleases the inner geek in me.

@derekdreery I don't think that would work well with imperative control flow like return and continue

Keep in mind that the semantics of the ? operator are already stable. Only the actual Try trait is unstable, and any changes must preserve the same stable effect for Option and Result.

@KrishnaSannasi

@derekdreery I don't think that would work well with imperative control flow like return and continue

I agree with this statement.

@cuviper

Keep in mind that the semantics of the ? operator are already stable. Only the actual Try trait is unstable, and any changes must preserve the same stable effect for Option and Result.

Is this true across epochs as well?

On a more general note, I don't think it's possible to unify concepts like .await, ?/early return, Option::map, Result::map, Iterator::map in rust, but understanding that these all share some structure definitely helps me to be a better programmer.

Apologies for being OT.

I'm not sure if an epoch/edition would be allowed to change ? desugaring, but that would certainly complicate a lot of cross-crate concerns like macros.

My interpretation of the stability guarantees and epochs RFC is that the ? behavior (sugar or otherwise) could be changed, but its current behavior on Option/Result types must stay the same because breaking that would create far more churn than we could ever hope to justify.

What if Option somehow was a type alias for Result? I.e. type Option<T> = Result<T, NoValue>, Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x), Option::<T>::None = Result::<T, NoValue>::Err(NoValue)? That'd take some magic to implement and not realistic in the current language environment, but maybe that worth exploring.

We can't make this change because there are trait impls that rely on Option an Result being distinct types.

If we ignore that, then we could take it one step further and even make bool an alias for Result<True, False>, where True and False are unit types.

Has it been considered to add a Try impl for unit, ()? For functions that don't return anything, an early return may still be useful in the case of an error in a non-critical function, such as a logging callback. Or, was unit excluded because it is preferred to never silently ignore errors in any situation?

Or, was unit excluded because it is preferred to never silently ignore errors in any situation?

This. If you want to ignore errors in a non-critical context you should use unwrap or one of it's variants.

being able to use ? on something like foo() -> () would be quite useful in a conditional compilation context and should strongly be considered. I think that is different from your question though.

you should use unwrap or one of it's variants.

unwrap() causes a panic, so I wouldn't recommend using it in non-critical contexts. It wouldn't be appropriate in situations where a simple return is desired. One could make an argument that ? is not totally "silent" because of the explicit, visible use of ? in the source code. In this way ? and unwrap() are analogous for unit functions, just with different return semantics. The only difference I see is that unwrap() will make visible side-effects (abort the program / print a stacktrace) and ? would not.

Right now, the ergonomics of early returning in unit functions is considerably worse than in those returning Result or Option. Perhaps this state of affairs is desirable because users should use a return type of Result or Option in these cases, and this provides incentive for them to change their API. Either way, it may be good to include a discussion of the unit return type in the PR or documentation.

being able to use ? on something like foo() -> () would be quite useful in a conditional compilation context and should strongly be considered. I think that is different from your question though.

How would this work? Just always evaluate to Ok(()) and be ignored?

Also, Can you give an example of where you'd want to use this?

Yeah the idea was that something like MyCollection::push might, depending on compile time config, have either a Result<(), AllocError> return value or a () return value if the collection is configured to just panic on error. Intermediate code using the collection shouldn't have to think about that, so if it could simply _always_ use ? even when the return type is () it would be handy.

After almost 3 years, is this any closer to being resolved?

@Lokathor that would only work if a return type Result<Result<X,Y>,Z> or similiar wasn't possible. But it is. Thus not possible.

I don't understand why a layered Result causes problems. Could you please elaborate?

For cross-referencing purposes, an alternative formulation has been proposed on internals by @dureuill:

https://internals.rust-lang.org/t/a-slightly-more-general-easier-to-implement-alternative-to-the-try-trait/12034

@Lokathor
Ok, I thought about it more deeply and think I might have found a rather good explanation.

Using an annotation

The problem is the interpretation of the return type or weird annotations would clutter the code. It would be possible, but it would make code harder to read. (Precondition: #[debug_result] does apply your wanted behavoir, and modifies a function to panic in release mode instead of returning an Result::Err(...), and unwraps Result::Ok, but that part is tricky)

#[debug_result]
fn f() -> Result<X, Y>;

#[debug_result]
fn f2() -> Result<Result<A, B>, Y>;

#[debug_result]
fn g() -> Result<X, Y> {
    // #[debug_result_spoiled]
    let w = f();
    // w could have type X or `Result<X,Y>` based on a #[cfg(debug_...)]
    // the following line would currently only work half of the time
    // we would modify the behavoir of `?` to a no-op if #[cfg(not(debug_...))]
    // and `w` was marked as `#[debug_result]`-spoiled
    let x = w?;

    // but it gets worse; what's with the following:
    let y = f2();
    let z = y?;
    // it has completely different sematics based on a #[cfg(debug_...)],
    // but would (currently?) print no warnings or errors at all,
    // and the type of z would be differently.

    Ok(z)
}

Thus, it would make the code harder to read.
We can't simply modify the behavoir of ? simply to a no-op,
especially if the return value of a #[debug_result] is saved in a temporary variable and later "try-propagated" with the ? operator. It would make the semantics of the ? really confusing, because it would depend on many "variables", which aren't necessarily known at "writing time" or could be hard to guess via just reading the source code. #[debug_result] would need to spoil variables which are assigned function values, but it won't work if one function is marked with #[debug_result] and one isn't, e.g. the following would be a type error.

// f3 is not annotated
fn f3() -> Result<X, Y>;

// later inside of a function:
   // z2 needs to be both marked spoiled and non-spoiled -> type error
   let z2 = if a() {
       f3()
   } else {
       f()
   };
   // althrough the following would be possible:
   let z2_ = if a() {
       f3()?
   } else {
       f()?
   };

Alternative: using a new type

A "cleaner" solution might be a DebugResult<T, E> type that just panics when a certain compile flag is set and it is constructed from an error, but would be otherwise equivalent to Result<T, E> otherwise. But that would be possible with the current proposal, too, I think.

Reply to @zserik s last post: The macro you described is pointless - charging return type of function based on build configuration will break every match on function result in every possible build configuration except the only one, regardless of way it was done.

@tema3210 I know. I only wanted to point out that @Lokathor's idea wouldn't generally work in practise. The only thing that might partially work is the following, but only with many restrictions, which I don't think is worth it in long-term:

// the result of the fn *must* have Try<Ok=()>
// btw. the macro could be already implemented with a proc_macro today.
#[debug_result]
fn x() -> Result<(), XError> {
  /* ..{xbody}.. */
}

// e.g. evaluates roughly to:
//..begin eval
fn x_inner() -> Result<(), XError> {
  /* ..{xbody}.. */
}

#[cfg(panic_on_err)]
fn x() {
  let _: () = x_inner().unwrap();
}
#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

//..end eval

fn y() -> Result<(), YError> {
  /* ... */
  // #[debug_result] results can't be matched on and can't be assigned to a
  // variable, they only can be used together with `?`, which would create
  // an asymetry in the type system, but could otherwise work, althrough
  // usage would be extremely restricted.
  x()?;
  // e.g. the following would fail to compile because capturing the result
  // is not allowed
  if let Err(z) = x() {
    // ...
  }
}

@zserik Is it possible that it actually takes a form like this?

#[cfg(panic_on_err)]
fn x() -> Result<(), !> {
  let _: () = x_inner().unwrap();
}

#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

ok, good idea.

I'm not really sure if this is something that needs to be accounted for prior to stabilization but I'm interested in implementing error return traces and I think it involves changes to the Try trait or at least its provided impl for Result to get it to work. Eventually I plan on turning this into an RFC but I want to make sure that the try trait isn't stabilized in a way that makes this impossible to add later incase it takes me a while to get around to writing said RFC. The basic idea is this.

You have a trait that you use to pass tracking info into which uses specialization and a default impl for T to maintain backwards compatibility

pub trait Track {
    fn track(&mut self, location: &'static Location<'static>);
}

default impl<T> Track for T {
    fn track(&mut self, _: &'static Location<'static>) {}
}

And then you modify the Try implementation for Result to use track_caller and pass this information into the inner type,

    #[track_caller]
    fn from_error(mut v: Self::Error) -> Self {
        v.track(Location::caller());
        Self::Err(v)
    }

And then for types that you want to gather backtraces for you implement Track

#[derive(Debug, Default)]
pub struct ReturnTrace {
    frames: Vec<&'static Location<'static>>,
}

impl Track for ReturnTrace {
    fn track(&mut self, location: &'static Location<'static>) {
        self.frames.push(location);
    }
}

Usage ends up looking like this

#![feature(try_blocks)]
use error_return_trace::{MyResult, ReturnTrace};

fn main() {
    let trace = match one() {
        MyResult::Ok(()) => unreachable!(),
        MyResult::Err(trace) => trace,
    };

    println!("{:?}", trace);
}

fn one() -> MyResult<(), ReturnTrace> {
    try { two()? }
}

fn two() -> MyResult<(), ReturnTrace> {
    MyResult::Err(ReturnTrace::default())?
}

And the output of a very shitty version of a backtrace looks like this

ReturnTrace { frames: [Location { file: "examples/usage.rs", line: 18, col: 42 }, Location { file: "examples/usage.rs", line: 14, col: 16 }] }

And here is a proof of concept of it in action https://github.com/yaahc/error-return-traces

I thought that only one error type which we can convert to try trait implementer might be insufficient, so, we could provide interface like this:

trait Try{
     type Error;
     type Ok;
     fn into_result(self)->Result<Self::Ok,Self::Error>;
     fn from_ok(val: Self::Ok)->Self;
     fn from_error<T>(val: T)->Self;
}

Note, there compiler can monomorfize from_error, avoiding From::from call, and one can provide method impl for different error types manually, resulting in ability of ? operator to "unwrap" different types of errors.

 fn from_error<T>(val: T)->Self;

As written, the implementer would have to accept _any_ sized T, unconstrained. If you meant to let this be custom constrained, it would have to be a trait parameter, like Try<T>. This is similar to the TryBlock/Bubble<Other> combination that @scottmcm had in https://github.com/rust-lang/rust/issues/42327#issuecomment-457353299.

As written, the implementer would have to accept _any_ sized T, unconstrained. If you meant to let this be custom constrained, it would have to be a trait parameter, like Try<T>. This is similar to the TryBlock/Bubble<Other> combination that @scottmcm had in #42327 (comment).

I meant that usage should look like this:

trait Try{
     //same from above
}
struct Dummy {
    a: u8,
}
struct Err1();
struct Err2();
impl Try for Dummy {
    type Ok=();
    type Error=();

    fn into_result(self)->Result<Self::Ok,Self::Error>{
        std::result::Result::Ok(())
    }
    fn from_ok(val: Self::Ok)->Self{
        Self{a: 0u8}
    }
    fn from_error<T>(val: Err1)->Self where T == Err1{
        Self{a: 1u8}
    }
    fn from_error<T>(val: Err2)->Self where T == Err2{
        Self{a: 2u8}
    }
}

you'd need to split the Try and the TryFromError. I do like that more than the original proposal fwiw but I think it'd need a new RFC.

(and I still think it should've been called "propagate" instead of "try", but I digress)

@tema3210 I think I understand your intent, but that's not valid Rust.

@SoniEx2

you'd need to split the Try and the TryFromError.

Right, that's why I mentioned the TryBlock/Bubble<Other> design. We can debate that naming choice, but the idea was that early-return is not always about _errors_, per se. For example, a lot of the internal Iterator methods are using a custom LoopState type. For something like find, it's not an error to find what you're looking for, but we want to stop iteration there.

https://github.com/rust-lang/rust/blob/3360cc3a0ea33c84d0b0b1163107b1c1acbf2a69/src/libcore/iter/mod.rs#L375-L378

We can debate that naming choice, but the idea was that early-return is not always about errors, per se.

precisely why I don't like the name "try" and would prefer the name "propagate", because it "propagates" an "early" return :p

(not sure if this makes sense? last time I brought this up the "propagate" thing only made sense to me for some reason and I was never quite able to explain it to others.)

Will this trait be of any help when trying to overwrite the default ? behavior f.e. to add a log hook f.e. to log debug information (like file/line number)?

Currently it's supported to overwrite stdlib macros, but it seems that the ? operator doesn't get converted to the try! macro explicitly. That's unfortunate.

@stevenroose To add support for that solely to the Try trait, it would require a modification of the Try trait to include file location information about the location where ? "happened".

@stevenroose To add support for that solely to the Try trait, it would require a modification of the Try trait to include file location information about the location where ? "happened".

This is not true, #[track_caller] can be used on traits impls even if the trait definition doesn't include the annotation

@stevenroose to answer your question, yes, though if you're interested in printing all the ? locations an error propogates through you should check out the error return trace comment from above

https://github.com/rust-lang/rust/issues/42327#issuecomment-619218371

I'm not sure if anyone have already mentioned this, shall we impl Try for bool, maybe Try<Ok=(), Error=FalseError>?
So that we could do thing like this

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.is_valid()?; // bool
  x.transform()?; // Result
  true
}

Now I have to use return type Option<()> in most cases where I think ? could make code much more clear.

I'm not sure if anyone have already mentioned this, shall we impl Try for bool, maybe Try<Ok=(), Error=FalseError>?

This would make Try behave as the && operator on bools. As others have pointed out above, there are also use cases for short-circuiting on success, in which case Try should behave as ||. Since the && and || operators aren't much longer to type out, I also don't see much of an advantage to having this implementation.

@calebsander thanks for kindly reply.
That's true for some simple cases, but I don't think it is often the case, especially we could never have assignment statment like let x = v.get_mut(key)? in expression.
If && and || would always do the trick, we could as well play with .unwrap_or_else(), .and_then() for Option and Error cases.
Could you express the flowing code in && and ||?

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.not_valid().not()?; // bool
  for i in x.iter() {
    if i == 1 { return true }
    if i == 2 { return false }
    debug!("get {}" i);
  }
  let y = x.transform()?; // Result
  y == 1
}

For some condition that true means failed while false means success, !expr? might be confusing, but we could use expr.not()? to do the trick (note: for ops::Try, we always have false for Error)

That's true for some simple cases, but I don't think it is often the case, especially we could never have assignment statment like let x = v.get_mut(key)? in expression.

Well, unless I misunderstand your proposal, just implementing Try<Ok = (), Error = FalseError> for bool wouldn't allow this. You would also need to impl From<NoneError> for FalseError so that the ? operator could convert None into false. (And similarly, if you want to apply ? to a Result<T, E> inside a function that returns bool, you would need to impl From<E> for FalseError. I think such a blanket implementation would be problematic.) You could also just use some_option().ok_or(false)? and some_result().map_err(|_| false)? if you're okay with the return value Result<bool, bool> (which you can collapse with .unwrap_or_else(|err| err)).

Leaving issues of converting other Try errors to bool aside, the Try implementation you are proposing for bool is just the && operator. For example, these are equivalent

fn using_try() -> bool {
    some_bool()?;
    some_bool()?;
    some_bool()
}

and

fn using_and() -> bool {
    some_bool() &&
    some_bool() &&
    some_bool()
}

(Admittedly, control flows like if and loop which return non-() are less straightforward to translate.)

For some condition that true means failed while false means success, !expr? might be confusing, but we could use expr.not()? to do the trick (note: for ops::Try, we always have false for Error)

It's not clear to me that false should represent the Error case. (For example, Iterator.all() would want false to short-circuit, but Iterator::any() would want true instead.) As you pointed out, you can get the opposite short-circuiting behavior by inverting the value passed to ? (and inverting the function's return value as well). But I don't think that results in very readable code. It might make more sense to have separate structs And(bool) and Or(bool) that implement the two different behaviors.

And similarly, if you want to apply ? to a Result inside a function that returns bool, you would need to impl From for FalseError. I think such a blanket implementation would be problematic.

No, I don't want to impl From<T> for FalseError, maybe we could do result.ok()?

It's not clear to me that false should represent the Error case.

I think it somehow natural when we have bool::then that map true to Some.

Forgive me if I missed it - why doesnt NoneError impl std::error::Error? This makes it useless for any functions returning Box<dyn Error> or similar.

Forgive me if I missed it - why doesnt NoneError impl std::error::Error? This makes it useless for any functions returning Box<dyn Error> or similar.

Not an expert here, but I can see significant problems with trying to come up with a useful error message for "something was None and you expected it to be Some" (which is basically what you'd be gaining here -- some diagnostic message). In my experience, it has always made most sense to use the Option::ok_or_else combinator to use another error type instead, because as the calling code I generally have much better context to give anyways.

I agree with @ErichDonGubler. It's super annoying to be unable to use ? with Option, however it's for a good reason, and, as a language user, I think it's in everyone's best interest to handle errors properly. Doing this extra work of binding error context to None instead of just doing ? is really, really annoying, but it forces properly communicating errors, which is well worth it.

The only time I'd allow None be interpreted as an error is at very rough prototyping. I figured, if we add something like a Option::todo_err() call, that would return Ok of the internal value, but will panic on None. It is very counter-intuitive until you analyze the needs of the "rough prototyping" mode of code authoring. It's very similar to Ok(my_option.unwrap()), but doesn't need an Ok(...) wrapping. In addition, it explicitly specifies the "todo" nature, thus communicating the intent to remove this code from the production logic, replacing it with a proper error binding.

but will panic on None

Strongly feel that we should just leave this to unwrap. If we added a todo_err it should return an actual error type, which begs the question of what error type to return.

Also I suspect fn todo_err(self) -> Result<Self, !> would have the obvious problem of requiring ! to stabilize, which uh, well, Someday.

My usecase is indeed rough prototyping where I don't care about the errors too much. Doesn't the whole NoneError suffer from these problems you listed? If it is decided that it should exist (which I think is a good thing, at least for prototyping), I believe it should impl Error as it is named to be one.

Because of the type lacking this impl I resorted to just slapping a .ok_or("error msg") on it, which works also but is less convenient.

Was this page helpful?
0 / 5 - 0 ratings