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

Comments

Projects
None yet
@scottmcm
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.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Jun 28, 2017

Contributor

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)

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.

Show comment
Hide comment
@scottmcm

scottmcm Jun 28, 2017

Member

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

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.

Show comment
Hide comment
@fluffysquirrels

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

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.

Show comment
Hide comment
@skade

skade Jul 30, 2017

Contributor

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.

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.

Show comment
Hide comment
@cuviper

cuviper Oct 25, 2017

Member

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.

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.

Show comment
Hide comment
@fluffysquirrels

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

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.

Show comment
Hide comment
@ErichDonGubler

ErichDonGubler Dec 29, 2017

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

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

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Dec 29, 2017

Member

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

Member

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.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 20, 2018

Contributor

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.

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.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Feb 20, 2018

Contributor

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

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.

Show comment
Hide comment
@kevincox

kevincox 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),
		}
	}
}

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.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 20, 2018

Contributor

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.

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.

Show comment
Hide comment
@scottmcm

scottmcm Mar 28, 2018

Member

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)

Member

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.

Show comment
Hide comment
@tmccombs

tmccombs Mar 31, 2018

Contributor

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.

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.

Show comment
Hide comment
@scottmcm

scottmcm Apr 2, 2018

Member

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

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.

Show comment
Hide comment
@cowang4

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

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.

Show comment
Hide comment
@scottmcm

scottmcm Apr 3, 2018

Member

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

Member

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.

Show comment
Hide comment
@cowang4

cowang4 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(_);
}

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.

Show comment
Hide comment
@tmccombs

tmccombs Apr 3, 2018

Contributor

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

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.

Show comment
Hide comment
@cowang4

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

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.

Show comment
Hide comment
@tmccombs

tmccombs Apr 4, 2018

Contributor

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.

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.

Show comment
Hide comment
@cowang4

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

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

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Apr 4, 2018

Member

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

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

Member

scottmcm commented Apr 4, 2018

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

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

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Apr 6, 2018

Member

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

Member

scottmcm commented Apr 6, 2018

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

@tmccombs

This comment has been minimized.

Show comment
Hide comment
@tmccombs

tmccombs Apr 6, 2018

Contributor

@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);
}
Contributor

tmccombs commented Apr 6, 2018

@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);
}
@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Apr 9, 2018

Contributor

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

Contributor

nikomatsakis commented Apr 9, 2018

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

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Apr 10, 2018

Member

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 rust-lang/rfcs#1859 (comment):

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

Member

scottmcm commented Apr 10, 2018

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 rust-lang/rfcs#1859 (comment):

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

@tmccombs

This comment has been minimized.

Show comment
Hide comment
@tmccombs

tmccombs Apr 10, 2018

Contributor

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.

Contributor

tmccombs commented Apr 10, 2018

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.

@illustrious-you

This comment has been minimized.

Show comment
Hide comment
@illustrious-you

illustrious-you Apr 11, 2018

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

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

@tmccombs

This comment has been minimized.

Show comment
Hide comment
@tmccombs

tmccombs Apr 11, 2018

Contributor

I suspect "raise" is a better fit

good point

Contributor

tmccombs commented Apr 11, 2018

I suspect "raise" is a better fit

good point

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Apr 11, 2018

Contributor

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

Contributor

nikomatsakis commented Apr 11, 2018

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

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Apr 12, 2018

Member

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

Member

scottmcm commented Apr 12, 2018

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

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Apr 18, 2018

Contributor

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

Contributor

glaebhoerl commented Apr 18, 2018

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

@tmccombs

This comment has been minimized.

Show comment
Hide comment
@tmccombs

tmccombs Apr 18, 2018

Contributor

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

Contributor

tmccombs commented Apr 18, 2018

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

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Apr 19, 2018

Contributor

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

Contributor

glaebhoerl commented Apr 19, 2018

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

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Apr 20, 2018

Member

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

Member

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.

Show comment
Hide comment
@SergioBenitez

SergioBenitez Apr 22, 2018

Contributor

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.

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.

Show comment
Hide comment
@scottmcm

scottmcm Apr 23, 2018

Member

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

Member

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.

Show comment
Hide comment
@SergioBenitez

SergioBenitez Apr 25, 2018

Contributor

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.

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.

Show comment
Hide comment
@scottmcm

scottmcm May 3, 2018

Member

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

Member

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.

Show comment
Hide comment
@SoniEx2

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

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.

Show comment
Hide comment
@joshtriplett

joshtriplett May 31, 2018

Member

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

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.

Show comment
Hide comment
@jmillikin

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

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.

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