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 · 52 comments

Comments

Projects
None yet
@scottmcm
Copy link
Member

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

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Member

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

ErichDonGubler commented Dec 29, 2017

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

@scottmcm

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Apr 19, 2018

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

@scottmcm

This comment has been minimized.

Copy link
Member Author

scottmcm commented Apr 20, 2018

The try{} RFC (rust-lang/rfcs#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 (rust-lang/rfcs#2367).

@SergioBenitez

This comment has been minimized.

Copy link
Contributor

SergioBenitez commented Apr 22, 2018

I've had similar issues as those evidenced by @nikomatsakis using the existing version of the Try trait. For the specific issues, see SergioBenitez/Rocket#597 (comment).

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.

@scottmcm

This comment has been minimized.

Copy link
Member Author

scottmcm commented Apr 23, 2018

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" rust-lang/rfcs#1859 (comment)) and an advantage of the final reductionist proposal ("For any given type T, ? can produce exactly one kind of ok/error value" rust-lang/rfcs#1859 (comment)). 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)).

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:

let mut accum = init;
while let Some(x) = self.next() {
accum = f(accum, x)?;
}
Try::from_ok(accum)

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 🙂

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

@SergioBenitez

This comment has been minimized.

Copy link
Contributor

SergioBenitez commented Apr 25, 2018

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.

@scottmcm

This comment has been minimized.

Copy link
Member Author

scottmcm commented May 3, 2018

@SergioBenitez From rust-lang/rfcs#1859 (comment)

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.

@SoniEx2

This comment has been minimized.

Copy link

SoniEx2 commented May 5, 2018

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.

@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented May 31, 2018

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

@jmillikin

This comment has been minimized.

Copy link

jmillikin commented Aug 10, 2018

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.

@brunoczim

This comment has been minimized.

Copy link

brunoczim commented Oct 20, 2018

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

@DJMcNab

This comment has been minimized.

Copy link

DJMcNab commented Dec 8, 2018

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.

@fschutt

This comment has been minimized.

Copy link
Contributor

fschutt commented Dec 20, 2018

Repost from #31436 (comment) 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.

@scottmcm

This comment has been minimized.

Copy link
Member Author

scottmcm commented Jan 24, 2019

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

@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Jan 24, 2019

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

@cuviper

This comment has been minimized.

Copy link
Member

cuviper commented Feb 25, 2019

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

@omarabid

This comment has been minimized.

Copy link

omarabid commented Mar 4, 2019

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?

@cuviper

This comment has been minimized.

Copy link
Member

cuviper commented Mar 4, 2019

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

@cuviper

This comment has been minimized.

Copy link
Member

cuviper commented Mar 4, 2019

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))),
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.