Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Tracking issue for `ops::Try` (`try_trait` feature) #42327

Open
scottmcm opened this issue May 31, 2017 · 89 comments
Open

Tracking issue for `ops::Try` (`try_trait` feature) #42327

scottmcm opened this issue May 31, 2017 · 89 comments

Comments

@scottmcm
Copy link
Member

@scottmcm scottmcm commented May 31, 2017

The Try trait from rust-lang/rfcs#1859; implemented in PR #42275.

Split off from #31436 for clarity (per #42275 (comment))

  • As part of stabilizing, re-open PR #62606 to document implementing try_fold for iterators
@glaebhoerl
Copy link
Contributor

@glaebhoerl glaebhoerl commented Jun 28, 2017

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)

@scottmcm
Copy link
Member Author

@scottmcm scottmcm commented Jun 28, 2017

@glaebhoerl

  • Error vs Err was discussed related to TryFrom in #33417 (comment) and #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.
@fluffysquirrels
Copy link

@fluffysquirrels fluffysquirrels commented Jun 30, 2017

I'm not sure if this is the correct forum for this comment, please redirect me if it's not 😃. Perhaps this should have been on rust-lang/rfcs#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!

@skade
Copy link
Contributor

@skade skade commented Jul 30, 2017

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.

@cuviper
Copy link
Member

@cuviper cuviper commented Oct 25, 2017

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.

@fluffysquirrels
Copy link

@fluffysquirrels fluffysquirrels commented Oct 28, 2017

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.

@ErichDonGubler
Copy link

@ErichDonGubler ErichDonGubler commented Dec 29, 2017

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

@scottmcm
Copy link
Member Author

@scottmcm scottmcm commented Dec 29, 2017

@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.

@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Feb 20, 2018

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.

@glaebhoerl
Copy link
Contributor

@glaebhoerl glaebhoerl commented Feb 20, 2018

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.)

@kevincox
Copy link
Contributor

@kevincox kevincox commented Feb 20, 2018

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),
		}
	}
}
@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Feb 20, 2018

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.

@scottmcm
Copy link
Member Author

@scottmcm scottmcm commented Mar 28, 2018

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 😆 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 #42327 (comment) above

Edit 3: Seems like a Continue variant was also proposed in rust-lang/rfcs#1859 (comment)

@tmccombs
Copy link
Contributor

@tmccombs tmccombs commented Mar 31, 2018

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.

@scottmcm
Copy link
Member Author

@scottmcm scottmcm commented Apr 2, 2018

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

fn find<P>(&mut self, mut predicate: P) -> Option<Self::Item> where
Self: Sized,
P: FnMut(&Self::Item) -> bool,
{
self.try_for_each(move |x| {
if predicate(&x) { LoopState::Break(x) }
else { LoopState::Continue(()) }
}).break_value()
}

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() 
@cowang4
Copy link

@cowang4 cowang4 commented Apr 3, 2018

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

@scottmcm
Copy link
Member Author

@scottmcm scottmcm commented Apr 3, 2018

@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...

@cowang4
Copy link

@cowang4 cowang4 commented Apr 3, 2018

@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(_);
}
@tmccombs
Copy link
Contributor

@tmccombs tmccombs commented Apr 3, 2018

@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.

@cowang4
Copy link

@cowang4 cowang4 commented Apr 3, 2018

@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()

@tmccombs
Copy link
Contributor

@tmccombs tmccombs commented Apr 4, 2018

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.

@cowang4
Copy link

@cowang4 cowang4 commented Apr 4, 2018

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.

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! 😄

@RustyYato
Copy link
Contributor

@RustyYato RustyYato commented Dec 14, 2019

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.

@dcsommer
Copy link

@dcsommer dcsommer commented Jan 29, 2020

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?

@RustyYato
Copy link
Contributor

@RustyYato RustyYato commented Jan 29, 2020

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.

@Lokathor
Copy link
Contributor

@Lokathor Lokathor commented Jan 29, 2020

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.

@dcsommer
Copy link

@dcsommer dcsommer commented Jan 29, 2020

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.

@yaahc
Copy link
Contributor

@yaahc yaahc commented Jan 29, 2020

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?

@Lokathor
Copy link
Contributor

@Lokathor Lokathor commented Jan 29, 2020

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.

@bradjc bradjc mentioned this issue Mar 9, 2020
11 of 18 tasks complete
@kotauskas
Copy link

@kotauskas kotauskas commented Mar 24, 2020

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

@zserik
Copy link

@zserik zserik commented Mar 25, 2020

@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.

@Lokathor
Copy link
Contributor

@Lokathor Lokathor commented Mar 25, 2020

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

@djc
Copy link
Contributor

@djc djc commented Mar 27, 2020

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

@zserik
Copy link

@zserik zserik commented Mar 27, 2020

@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.

@tema3210
Copy link

@tema3210 tema3210 commented Apr 20, 2020

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.

@zserik
Copy link

@zserik zserik commented Apr 21, 2020

@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() {
    // ...
  }
}
@Kixiron
Copy link
Member

@Kixiron Kixiron commented Apr 21, 2020

@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()
}
@zserik
Copy link

@zserik zserik commented Apr 21, 2020

ok, good idea.

@yaahc
Copy link
Contributor

@yaahc yaahc commented Apr 24, 2020

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

@tema3210
Copy link

@tema3210 tema3210 commented Apr 24, 2020

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.

@cuviper
Copy link
Member

@cuviper cuviper commented Apr 24, 2020

 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 #42327 (comment).

@tema3210
Copy link

@tema3210 tema3210 commented Apr 25, 2020

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}
    }
}
@SoniEx2
Copy link

@SoniEx2 SoniEx2 commented Apr 25, 2020

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)

@cuviper
Copy link
Member

@cuviper cuviper commented Apr 25, 2020

@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.

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

@SoniEx2
Copy link

@SoniEx2 SoniEx2 commented Apr 25, 2020

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.)

@stevenroose
Copy link

@stevenroose stevenroose commented Apr 28, 2020

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.

@zserik
Copy link

@zserik zserik commented Apr 28, 2020

@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".

@yaahc
Copy link
Contributor

@yaahc yaahc commented Apr 28, 2020

@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

#42327 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.