The Try
trait from https://github.com/rust-lang/rfcs/pull/1859; implemented in PR https://github.com/rust-lang/rust/pull/42275.
Split off from https://github.com/rust-lang/rust/issues/31436 for clarity (per https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)
Iterator::try_fold
fold
be implemented in terms of try_fold
, so that both don't need to be overridden.)A couple of pieces of bikeshedding:
Do we have a particular motivation to call the associated type Error
instead of Err
? Calling it Err
would make it line up with Result
: the other one is already called Ok
.
Do we have a particular motivation for having separate from_error
and from_ok
methods, instead of a single from_result
which would be more symmetric with into_result
which is the other half of the trait?
(updated version of playground link from the RFC)
@glaebhoerl
Error
vs Err
was discussed related to TryFrom
in https://github.com/rust-lang/rust/issues/33417#issuecomment-269108968 and https://github.com/rust-lang/rust/pull/40281; I assume the name was chosen here for similar reasons.Result
that they're trying to turn into a T:Try
. I prefer Try::from_ok
and Try::from_error
to always calling Try::from_result(Ok(
and Try::from_result(Err(
, and I'm happy to just impl the two methods over writing out the match. Perhaps that's because I think of into_result
not as Into<Result>
, but as "was it pass or fail?", with the specific type being Result
as an unimportant implementation detail. (I don't want to suggest or reopen the "there should be a new type for produce-value vs early-return, though.) And for documentation, I like that from_error
talks about ?
(or eventually throw
), while from_ok
talks about success-wrapping (#41414), rather than having them both in the same method.I'm not sure if this is the correct forum for this comment, please redirect me if it's not :smiley:. Perhaps this should have been on https://github.com/rust-lang/rfcs/pull/1859 and I missed the comment period; oops!
I was wondering if there is a case for splitting up the Try
trait or removing the into_result
method; to me currently Try
is somewhat like the sum of traits FromResult
(containing from_error
and from_ok
) and IntoResult
(containing into_result
).
FromResult
enables highly ergonomic early exit with the ?
operator, which I think is the killer use case for the feature. I think IntoResult
can already be implemented neatly with per-use-case methods or as Into<Result>
; am I missing some useful examples?
Following the Try
trait RFC, expr?
could desugar as:
match expr { // Removed `Try::into_result()` here.
Ok(v) => v,
Err(e) => return Try::from_error(From::from(e)),
}
The motivating examples I considered are Future
and Option
.
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
is a similar case. We implement from_ok
, from_err
naturally as in the Try
trait RFC, and could convert Option<T>
to Result<T, Missing>
simply with o.ok_or(Missing)
or a convenience method ok_or_missing
.
Hope that helps!
I'm maybe very late to all this, but it occured to me this weekend that ?
has a rather natural semantic in cases where you'd like to short-circuit on _success_.
fn fun() -> SearchResult<Socks> {
search_drawer()?;
search_wardrobe()
}
But in this case, the naming of the Try
traits methods don't fit.
It would widen the meaning of the ?
operator, though.
In prototyping a try_fold
for rayon, I found myself wanting something like Try::is_error(&self)
for the Consumer::full()
methods. (Rayon has this because consumers are basically push-style iterators, but we still want to short-circuit errors.) I instead had to store the intermediate values as Result<T::Ok, T::Err>
so I could call Result::is_err()
.
From there I also wished for a Try::from_result
to get back to T
at the end, but a match
mapping to T::from_ok
and T::from_error
isn't _too_ bad.
The Try
trait can provide a from_result
method for ergonomics if from_ok
and from_error
are required. Or vice versa. From the examples given I see a case for offering both.
Since PR #42275 has been merged, does that mean this issue has been resolved?
@skade Note that a type can define whichever as the short-circuiting one, like LoopState
does for some Iterator
internals. Perhaps that would be more natural if Try
talked about "continuing or breaking" instead of success or failure.
@cuviper The version I ended up needing was either the Ok
type or the Error
-in-original type. I'm hoping that destructure-and-rebuild is a general enough thing that it'll optimize well and a bunch of special methods on the trait won't be needed.
@ErichDonGubler This is a tracking issue, so isn't resolved until the corresponding code is in stable.
Experience report:
I've been a little frustrated trying to put this trait into practice. Several times now I've been tempted to define my own variant on Result
for whatever reason, but each time I've wound up just using Result
in the end, mostly because implementing Try
was too annoying. I'm not entirely sure if this is a good thing or not, though!
Example:
In the new solver for the Chalk VM, I wanted to have an enum that indicates the result of solving a "strand". This had four possibilities:
enum StrandFail<T> {
Success(T),
NoSolution,
QuantumExceeded,
Cycle(Strand, Minimums),
}
I wanted ?
, when applied to this enum, to unwrap "success" but propagated all other failures upward. However, in order to implement the Try
trait, I would have had to define a kind of "residual" type that encapsulates just the error cases:
enum StrandFail {
NoSolution,
QuantumExceeded,
Cycle(Strand, Minimums),
}
But once I've got this type, then I might as well make StrandResult<T>
an alias:
type StrandResult<T> = Result<T, StrandFail>;
and this is what I did.
Now, this doesn't necessarily seem worse than having a single enum -- but it does feel a bit odd. Usually when I write the documentation for a function, for example, I don't "group" the results by "ok" and "error", but rather talk about the various possibilities as "equals". For example:
/// Invoked when a strand represents an **answer**. This means
/// that the strand has no subgoals left. There are two possibilities:
///
/// - the strand may represent an answer we have already found; in
/// that case, we can return `StrandFail::NoSolution`, as this
/// strand led nowhere of interest.
/// - the strand may represent a new answer, in which case it is
/// added to the table and `Ok` is returned.
Note that I didn't say "we return Err(StrandFail::NoSolution)
. This is because the Err
just feels like this annoying artifact I have to add.
(On the other hand, the current definition would help readers to know what the behavior of ?
is without consulting the Try
impl.)
I guess this outcome is not that surprising: the current Try
impl forces you to use ?
on things that are isomorphic to Result
. This uniformity is no accident, but as a result, it makes it annoying to use ?
with things that are not basically "just Result
". (For that matter, it's basically the same problem that gives rise to NoneError
-- the need to artificially define a type to represent the "failure" of an Option
.)
I'd also note that, in the case of StrandFail
, I don't particularly want the From::from
conversion that ordinary results get, though it's not hurting me.
Is the associated Try::Error
type ever exposed to / used by the user directly? Or is it just needed as part of the desugaring of ?
? If the latter, I don't see any real problem with just defining it "structurally" - type Error = Option<Option<(Strand, Minimums)>>
or whatever. (Having to figure out the structural equivalent of the "failure half" of the enum
definition isn't great, but seems less annoying than having to rejigger the whole public-facing API.)
I don't follow either. I have successfully implemented Try for a type that had an internal error representation and it felt natural having Ok
and Error
being the same type. You can see the implementation here: https://github.com/kevincox/ecl/blob/8ca7ad2bc4775c5cfc8eb9f4309b2666e5163e02/src/lib.rs#L298-L308
In fact is seems like there is a fairly simple pattern for making this work.
impl std::ops::Try for Value {
type Ok = Self;
type Error = Self;
fn from_ok(v: Self::Ok) -> Self { v }
fn from_error(v: Self::Error) -> Self { v }
fn into_result(self) -> Result<Self::Ok, Self::Error> {
if self.is_ok() { Ok(val) } else { Err(val) }
}
}
If you want to unwrap the success something like this should work:
impl std::ops::Try for StrandFail<T> {
type Ok = T;
type Error = Self;
fn from_ok(v: Self::Ok) -> Self { StrandFail::Success(v) }
fn from_error(v: Self::Error) -> Self { v }
fn into_result(self) -> Result<Self::Ok, Self::Error> {
match self {
StrandFail::Success(v) => Ok(v),
other => Err(other),
}
}
}
Defining a structural type is possible but feels pretty annoying. I agree I could use Self
. It feels a bit wacky to me though that you can then use ?
to convert from StrandResult
to Result<_, StrandResult>
etc.
Great experience report, @nikomatsakis! I've also been dissatisfied with Try
(from the other direction).
TL/DR: I think FoldWhile
got this right, and we should double-down on the Break
-vs-Continue
interpretation of ?
instead talking about it in terms of errors.
Longer:
I keep using Err
for something closer to "success" than "error" because ?
is so convenient.
When using try_fold
to implement position
(and a bunch of other iterator methods), I got so confused which was as up when using Result
(my brain didn't like find
finding the thing being an Err
) that I made my own enum with Break
and Continue
variants instead.
When writing tree_fold1
in itertools, I ended up with a weird match
that the inner function always returns Err
. There's this odd disconnect that None
is an "error" from Iterator::next()
, but at the same time it's "success" from a fold
, since you need to get to the end to be done. And it's very convenient to use ?
(well, try!
in that code since it needs to compile on Rust 1.12) to deal with propagating the end condition.
So if nothing else, I think the description I wrote for Try
is wrong, and shouldn't talk about a "success/failure dichotomy", but rather abstract away from "errors".
The other thing I've been thinking about is that we should consider some slightly-crazy impls for Try
. For example, Ordering: Try<Ok = (), Error = GreaterOrLess>
, combined with "try functions", could allow cmp
for struct Foo<T, U> { a: T, b: U }
to just be
fn cmp(&self, other: &self) -> Ordering try {
self.a.cmp(&other.a)?;
self.b.cmp(&other.b)?;
}
I don't yet know whether that's the good kind of crazy or the bad kind :laughing: It's certainly got some elegance to it, though, since once things are different you know they're different. And trying to assign "success" or "error" to either side of that doesn't seem to fit well.
I do also note that that's another instance where the "error-conversion" isn't helpful. I also never used it in the "I want ? but it's not an error" examples above. And it's certainly known to cause inference sadness, so I wonder if it's a thing that should get limited to just Result (or potentially removed in favour of .map_err(Into::into)
, but that's probably infeasible).
Edit: Oh, and all that makes me wonder if perhaps the answer to "I keep using Result for my errors instead of implementing Try
for a type of my own" is "good, that's expected".
Edit 2: This is not unlike https://github.com/rust-lang/rust/issues/42327#issuecomment-318923393 above
Edit 3: Seems like a Continue
variant was also proposed in https://github.com/rust-lang/rfcs/pull/1859#issuecomment-273985250
My two cents:
I like @fluffysquirrels's suggestion of splitting the trait into two traits. One for converting to a result, and another to convert from a result. But I do think we should keep the into_result
or equivalent as part of the desugaring. And I think at this point we have to since using Option
as a Try
has stabilized.
I also like @scottmcm's idea of using names that suggest break/continue rather than error/ok.
To put the specific code in here, I like how this reads:
It's of course not quite as nice as a straight loop, but with "try methods" it would be close:
self.try_for_each(move |x| try {
if predicate(&x) { return LoopState::Break(x) }
}).break_value()
For comparison, I find the "error vocabulary" version really misleading:
self.try_for_each(move |x| {
if predicate(&x) { Err(x) }
else { Ok(()) }
}).err()
Can we implement Display for NoneError? It would allow the failure crate to automatically derive From<NoneError> for failure::Error
. See https://github.com/rust-lang-nursery/failure/issues/61
It should be a 3 line change, but I'm not sure about the process of RFCs and such.
@cowang4 I'd like to try to avoid enabling any more mixing of Result-and-Option right now, as the type is unstable mostly to keep our options open there. I wouldn't be surprised if we ended up changing the design of Try
and the desugar into something that didn't need NoneError
...
@scottmcm Okay. I see your point. I'd eventually like a clean way to sugar the pattern of returning Err when an library function returns None. Maybe you know of one other than Try
? Example:
fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
if let Some(filestem) = pb.file_stem() {
if let Some(filestr) = filestem.to_str() {
return Ok(MyStruct {
filename: filestr.to_string()
});
}
}
Err(_)
}
Once I found this experimental feature and the failure
crate, I naturally gravitated to:
use failure::Error;
fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
Ok({
title: pb.file_stem?.to_str()?.to_string()
})
}
Which _almost_ works, except for the lack of a impl Display for NoneError
like I mentioned before.
But, if this isn't the syntax we'd like to go with, then maybe there could be another function / macro that simplifies the pattern:
if option.is_none() {
return Err(_);
}
@cowang4 I believe that would work if you implemented From<NoneError>
for failure::Error
, which used your own type that implemented Display
.
However, it is probably better practice to use opt.ok_or(_)?
so you can explicitly say what the error should be if the Option is None. In your example, for instance, you may want a different error if pb.file_stem
is None than if to_str()
returns None.
@tmccombs I tried creating my own error type, but I must've done it wrong. It was like this:
#[macro_use] extern crate failure_derive;
#[derive(Fail, Debug)]
#[fail(display = "An error occurred.")]
struct SiteError;
impl From<std::option::NoneError> for SiteError {
fn from(_err: std::option::NoneError) -> Self {
SiteError
}
}
fn build_piece(cur_dir: &PathBuf, piece: &PathBuf) -> Result<Piece, SiteError> {
let title: String = piece
.file_stem()?
.to_str()?
.to_string();
Ok(Piece {
title: title,
url: piece
.strip_prefix(cur_dir)?
.to_str()
.ok_or(err_msg("tostr"))?
.to_string(),
})
}
And then I tried using my error type...
error[E0277]: the trait bound `SiteError: std::convert::From<std::path::StripPrefixError>` is not satisfied
--> src/main.rs:195:14
|
195 | url: piece
| ______________^
196 | | .strip_prefix(cur_dir)?
| |___________________________________^ the trait `std::convert::From<std::path::StripPrefixError>` is not implemented for `SiteError`
|
= help: the following implementations were found:
<SiteError as std::convert::From<std::option::NoneError>>
= note: required by `std::convert::From::from`
error[E0277]: the trait bound `SiteError: std::convert::From<failure::Error>` is not satisfied
--> src/main.rs:195:14
|
195 | url: piece
| ______________^
196 | | .strip_prefix(cur_dir)?
197 | | .to_str()
198 | | .ok_or(err_msg("tostr"))?
| |_____________________________________^ the trait `std::convert::From<failure::Error>` is not implemented for `SiteError`
|
= help: the following implementations were found:
<SiteError as std::convert::From<std::option::NoneError>>
= note: required by `std::convert::From::from`
Okay, I just realized that it wants to know how to convert from other error types to my error, which I can write generically:
impl<E: failure::Fail> From<E> for SiteError {
fn from(_err: E) -> Self {
SiteError
}
}
Nope...
error[E0119]: conflicting implementations of trait `std::convert::From<SiteError>` for type `SiteError`:
--> src/main.rs:183:1
|
183 | impl<E: failure::Fail> From<E> for SiteError {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: conflicting implementation in crate `core`:
- impl<T> std::convert::From<T> for T;
Okay, what about std::error::Error
?
impl<E: std::error::Error> From<E> for SiteError {
fn from(_err: E) -> Self {
SiteError
}
}
That doesn't work either. Partly because it conflicts with my From<NoneError>
error[E0119]: conflicting implementations of trait `std::convert::From<std::option::NoneError>` for type `SiteError`:
--> src/main.rs:181:1
|
175 | impl From<std::option::NoneError> for SiteError {
| ----------------------------------------------- first implementation here
...
181 | impl<E: std::error::Error> From<E> for SiteError {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `SiteError`
|
= note: upstream crates may add new impl of trait `std::error::Error` for type `std::option::NoneError` in future versions
Which is weird because I thought that NoneError
didn't implement std::error::Error
. When I comment out my non-generic impl From<NoneError>
I get:
error[E0277]: the trait bound `std::option::NoneError: std::error::Error` is not satisfied
--> src/main.rs:189:25
|
189 | let title: String = piece
| _________________________^
190 | | .file_stem()?
| |_____________________^ the trait `std::error::Error` is not implemented for `std::option::NoneError`
|
= note: required because of the requirements on the impl of `std::convert::From<std::option::NoneError>` for `SiteError`
= note: required by `std::convert::From::from`
Do I have to write all the From
s manually. I though that the failure crate was supposed to derive them?
Maybe I should stick with option.ok_or()
Do I have to write all the Froms manually. I though that the failure crate was supposed to derive them?
I don't think the failure crate does that. But I could be wrong.
Ok, so I re-examined the failure crate, and if I'm reading the documentation and different versions right, it's designed to always use the failure::Error
as the Error type in your Result
s, see here. And, it does implement a blanket impl Fail
trait for most Error types:
impl<E: StdError + Send + Sync + 'static> Fail for E {}
https://github.com/rust-lang-nursery/failure/blob/master/failure-1.X/src/lib.rs#L218
And then a impl From
so that it can Try
/?
other errors (like ones from std) into the overarching failure::Error
type.
rust
impl<F: Fail> From<F> for ErrorImpl
https://github.com/rust-lang-nursery/failure/blob/d60e750fa0165e9c5779454f47a6ce5b3aa426a3/failure-1.X/src/error/error_impl.rs#L16
But, it's just that since the error relevant to this rust issue, NoneError
, is experimental, it can't be converted automatically yet, because it doesn't implement the Display
trait. And, we don't want it to, because that blurs the line between Option
s and Result
s more. It'll all probably get re-worked and sorted out eventually, but for now, I'll stick to the de-sugared techniques that I've learned.
Thank you everyone for helping. I'm slowly learning Rust! :smile:
I'll stick to the de-sugared techniques that I've learned.
:+1: Honestly, I think .ok_or(...)?
will remain the way to go (or .ok_or_else(|| ...)?
, of course). Even if NoneError
_had_ a Display
impl, what would it say? "Something wasn't there"? That's not a great error...
Attempting to move closer to a concrete proposal...
I'm starting to like the TrySuccess
alternative. Interestingly, I think _nobody_, myself included, liked that one originally -- it's not even in the final version of the RFC. But thankfully it lives on in the history: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#using-an-associated-type-for-the-success-value
It feels to me like the biggest objection to it was the reasonable complaint that a whole core trait for just an associated type (trait TrySuccess { type Success; }
) was overkill. But with catch
try
blocks back (https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777) to doing ok-wrapping (as in the RFC), all of a sudden it has a direct and important use: this is the trait that controls that wrapping. Coupled with the "?
should always produce the same type" goal that I think was generally desired, the trait seems to better hold its weight. Strawman:
trait TryContinue {
type Continue;
fn from_continue(_: Self::Continue) -> Self;
}
That associated type, as the one that will be returned from the ?
operator, is also the one that clearly needs to exist. That means it doesn't hit the "had to define a kind of 'residual' type" annoyance that Niko articulated. And it being ()
is reasonable, even common, so avoids NoneError
-like contortions.
Edit: For the bikeshed, "return" might be a good word, since this is what happens when you return
from a try
method (aka ok-wrapping). return
is also the monad operator name for this, iiuc...
Edit 2: My thoughts on the other trait(s)/method(s) haven't stabilized yet, @tmccombs.
@scottmcm just to be clear, or you suggesting the following two traits?
trait TryContinue {
type Continue;
fn from_continue(_: Self::Continue) -> Self;
}
trait Try<E>: TryContinue {
fn try(self) -> Result<Self::Continue, E>
}
And desuguring x?
would look something like:
x.try() match {
Ok(c) => c,
Err(e) => throw e // throw here is just a placeholder for either returning or breaking out of a try block
}
and try { ...; expr}
would desugar to something like:
{
...
TryContinue::from_continue(expr);
}
@scottmcm I too find that variant much more appealing when considering ok-wrapping =)
Continuing on to the next trait, I think the one for ?
becomes this (modulo massive bikeshedding):
trait Try<Other: TryContinue = Self>: TryContinue + Sized {
fn check(x: Other) -> ControlFlow<Other::Continue, Self>;
}
enum ControlFlow<C, B> {
Continue(C),
Break(B),
}
Assorted justification:
TryContinue
for its argument type so that the type of an x?
expression is always the sameSelf
for simple cases like try_fold
that inspect and return the same type, so are fine with just T: Try
.TryContinue
because if a type used as a return type allows ?
in its body, then it should also support ok-wrapping.?
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:
Result
impl that I don't really likeLessOrGreater
type in the Ordering
impl.From
transformStrandResult
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.throw
syntax would be?
as Ok(x) => x, Err(r) => throw e.into()
that I really likedthrow;
be the way to produce None
(via something like impl<T> Throw<()> for Option<T>
), which is way better than throw NoneError;
throw LessOrGreater::Less
would have been _really_ silly.Edit: Ping @glaebhoerl, whose opinion I'd like on this as a big participant in RFC 1859.
Edit 2: Also cc @colin-kiegel, for this statement from https://github.com/rust-lang/rfcs/pull/1859#issuecomment-287402652:
I wonder if the essentialist approach could adopt some of the elegance of the reductionists without sacrificing the goals above.
I really like that proposal.
It's not as obvious what throw syntax would be
Ignoring the baggage of the throw keyword from other languages, "throw" does sort of make sense, in that you are "throwing" the value up to a higher scope. Python and scala also do use exceptions for control flow other than error cases (though, in the case of scala you wouldn't usually use try/catch directly), so there is some precedent for using throw for successful paths.
@tmccombs
Ignoring the baggage of the throw keyword from other languages, "throw" does sort of make sense, in that you are "throwing" the value up to a higher scope.
Though it comes with similar baggage, I suspect "raise" is a better fit:
throw (v): to put or cause to go or come into some place, position, condition, etc., as if by hurling:
raise (v): to move to a higher position; lift up; elevate
There may be a way to combine "raise" with the logic of ?
, given that raise can also mean "collect". Something like: Ok(v) => v, Err(e) => raise From::from(e)
, where raise
mimics the matched pattern (e.g. given a pattern Err(e)
it is syntactic magic for ControlFlow::Break(Err(...))
).
I suspect "raise" is a better fit
good point
@scottmcm
It's not as obvious what throw syntax would be
Is there a reason we can't have from_err(value: Other)
?
UPDATE: Maybe I'm confused about the role of Other
, actually. I have to study this trait a bit more. =)
@nikomatsakis
Is there a reason we can't have
from_err(value: Other)
?
Well, Other
is a whole ?
-able type (like a Result
), so I wouldn't want throw Ok(4)
to work. (And it need to be the whole disjunction to avoid forcing the introduction of an artificial error type.) For example, I think our current Option-Result interop would be equivalent to this (and the inverse):
impl<T, F, U> Try<Option<U>> for Result<T, F>
where F: From<NoneError>
{
fn check(x: Option<U>) -> ControlFlow<U, Self> {
match x {
Some(x) => ControlFlow::Continue(x),
None => ControlFlow::Break(Err(From::from(NoneError))),
}
}
}
My current inclination for throw
would be for this:
trait TryBreak<T> : TryContinue { from_break(_: T) -> Self }
with
throw $expr ==> break 'closest_catch TryBreak::from_break($expr)
TryContinue
so that ok-wrapping is also available for the return type of the method in which it's usedTryBreak<TryFromIntError>
and TryBreak<io::Error>
for a type if you wanted.impl<T> TryBreak<()> for Option<T>
to enable just throw;
feels plausible in a way that an associated error type of ()
didn't.impl<T> TryBreak<!> for T
would be nice too, but probably is incoherent.(Aside: these trait and method names are terrible; please help!)
My thoughts about the other issues raised here haven't gelled into an easily articulable form yet, but with respect to the type inference problems around the From::from()
desugaring (I can't remember if this was also discussed somewhere else recently, or only here?):
The instinctual sense (from Haskell experience) that "that way lie type inference ambiguities" was one of (though not the main) reasons why I didn't want to have a From
conversion as part of the original RFC. Now that it's baked into the cake, though, I wonder if we couldn't try to have that cake and eat it too by 'just' adding a special defaulting rule for it to the type inferencing process?
That is, I think: whenever we see a From::from()
[optionally: one that was inserted by the ?
desugaring, rather than written manually], and we know exactly one of the two types (input vs. output), while the other one is ambiguous, we default the other to be the same as the one. In other words, I think, when it's not clear which impl From
to use, we default to impl<T> From<T> for T
. This is, I think, basically always what you actually want? It's maybe a bit ad-hoc, but if it does work, the benefits seem well worth the costs IMHO.
(I also thought From
was already a lang item, precisely due to the ?
desugaring, but it doesn't seem to be? In any case, it's already in some ways special for that reason.)
in @scottmcm 's proposal, From::from()
is _not_ part of the desugaring, rather it is in the implementation of Try
for Result
.
@tmccombs I wasn't proposing an amendment to his proposal.
The try{}
RFC (https://github.com/rust-lang/rfcs/pull/2388) has been discussing try
blocks where one doesn't care about the result. This alternative seems to handle that decently well, as it can choose to ignore the error types entirely if it so wishes, allowing
let IgnoreErrors = try {
error()?;
none()?;
};
Proof-of-concept implementation using the same traits as before: https://play.rust-lang.org/?gist=e0f6677632e0a9941ed1a67ca9ae9c98&version=stable
I think there's some interesting possibilities there, especially since a custom implementation could, say, only take results and bound E: Debug
so it automatically logs any error that happens. Or a version could be made intended specifically as a return type for main
in conjunction with Termination
that "just works" to let you use ?
without a complex type signature (https://github.com/rust-lang/rfcs/issues/2367).
I've had similar issues as those evidenced by @nikomatsakis using the existing version of the Try
trait. For the specific issues, see https://github.com/SergioBenitez/Rocket/issues/597#issuecomment-381533108.
The trait definitions proposed by @scottmcm resolve these issues. They seem to be more complicated than necessary, however. I took a crack at reimplementing them and came up with the following:
#[derive(Debug, Copy, Clone)]
enum ControlFlow<C, B> {
Continue(C),
Break(B),
}
// Used by `try { }` expansions.
trait FromTry: Try {
fn from_try(value: Self::Continue) -> Self;
}
// Used by `?`.
trait Try<T = Self>: Sized {
type Continue;
fn check(x: T) -> ControlFlow<Self::Continue, Self>;
}
The main change is that the Continue
associated type is on the Try
trait as opposed to the FromTry
(previously TryContinue
). Besides simplifying the definitions, this has the advantage that Try
can be implemented independent of FromTry
, which I posit to be more common, and that implementing FromTry
is simplified once Try
is implemented. (Note: if it's desired that Try
and FromTry
be implemented in unison, we can simply move the from_try
method into Try
)
See the complete playground with implementations for Result
and Option
as well as Rocket's Outcome
at this playground.
Thanks for the report, @SergioBenitez! That implementation matches the "flip the type parameters" alternative version of the original trait proposal in RFC 1859: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#unresolved-questions
The biggest thing that loses is the property that typeof(x?)
depends only on typeof(x)
. Lacking that property was one of the concerns with the original ("I worry a bit about readability in code along these lines" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967) and an advantage of the final reductionist proposal ("For any given type T, ? can produce exactly one kind of ok/error value" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310). Of course, there were also arguments that said property is unnecessary or too restrictive, but in the final summary before FCP it was still there as an advantage (https://github.com/rust-lang/rfcs/pull/1859#issuecomment-295878466).
Besides simplifying the definitions, this has the advantage that
Try
can be implemented independent ofFromTry
, 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
:
If a function's going to return a carrier type, it's critical that there's a way to put the value of interest into that carrier type. And, importantly, I don't think providing this is a hardship. It has a very natural definition for Result, for Option, for Outcome, etc.
@Centril has also pointed out before that "wrap a scalar in a carrier" is also a simpler construct theoretically. For examples, Haskell calls it the Pointed
typeclass, though I don't think we want to generalize it _that_ far in Rust: allowing try { 4 }
↦ vec![4]
seems like overkill to me.
I'm also imagining a future where, like async
functions are proposed to wrap the block value into a future, we might have try
functions that wrap the block value into a fallible type. There, again, the TryContinue
is the critical part -- such a function might not even use ?
, if we got a throw
construct.
So all that is, unfortunately, more philosophical than concrete. Let me know if it made any sense, or if you think the opposite in any of the parts :slightly_smiling_face:
Edit: Apologies if you got an email with an early version of this; I hit "comment" too early...
The biggest thing that loses is the property that typeof(x?) depends only on typeof(x).
Ah yes, absolutely. Thanks for pointing that out.
Of course, there were also arguments that said property is unnecessary or too restrictive, but in the final summary before FCP it was still there as an advantage (rust-lang/rfcs#1859 (comment)).
Are there any specific examples of where it might be too restrictive?
If a function's going to return a carrier type, it's critical that there's a way to put the value of interest into that carrier type. And, importantly, I don't think providing this is a hardship.
I think that's a fair analysis. I agree.
@SergioBenitez From https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967
So the question is, are the proposed restrictions enough? Are there good uses of error case context in determining the success type? Are there likely abuses?
I can say from my experience in futures that there may well be some useful cases here. In particular, the Poll type that you talk about can be processed in a couple ways. Sometimes, we want to jump out on the NotReady variant and be left with essentially a Result value. Sometimes, we're interested only in the Ready variant and want to jump out on either of the other variants (as in your sketch). If we allow the success type to be determined in part by the error type, it's more plausible to support both of these cases, and basically use the context to determine what kind of decomposition is desired.
OTOH, I worry a bit about readability in code along these lines. This feels qualitatively different than simply converting the error component -- it means that the basic match that ? would be performing is dependent on contextual information.
So one could imagine a trait that moved _both_ types to type parameters, like
trait Try<T,E> {
fn question(self) -> Either<T, E>;
}
And use that do enable all of
let x: Result<_,_> = blah.poll()?; // early-return if NotReady
let x: Async<_> = blah.poll()?; // early-return if Error
let x: i32 = blah.poll()?; // early-return both NotReady and Errors
But I think that's definitely a bad idea, since it means that these don't work
println!("{}", z?);
z?.method();
Since there's no type context to say what to produce.
The other version would be to enable things like this:
fn foo() -> Result<(), Error> {
// `x` is an Async<i32> because NotReady doesn't fit in Result
let x = something_that_returns_poll()?;
}
fn bar() -> Poll<(), Error> {
// `x` is just i32 because we're in a Poll-returning method
let x = something_that_returns_poll()?;
}
My instinct there is that the inference "flowing out of the ?" there is too surprising, and thus this is in the "too clever" bucket.
Critically, I don't think that not having it is too restrictive. my_result?
in a -> Poll
function doesn't need it, as the success type is the same as usual (important to keep synchronous code working the same in async contexts) and the error variant converts fine too. Using ?
on a Poll
in a method that returns Result
seems like an anti-pattern anyway, not something that should be common, so using (hypothetical) dedicated methods like .wait(): T
(to block for the result) or .ready(): Option<T>
(to check if it's done) to choose the mode is probably better anyway.
This is "interesting" https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug
I don't like these try (do catch) blocks, they don't seem very newcomer-friendly.
I'm trying to assemble the current state of the proposal, which seems spread across multiple comments. Is there a single summary of the currently proposed set of traits (which drop the Error
associated type)?
Early on in this thread I see a comment about splitting Try
into separate to/from error traits -- are there any plans to implement that split?
It would be useful to have transparent conversion from Result<T, E>
to any type Other<T2, E>
on question mark -- this would let existing IO functions be called with nice syntax from within a function with a more specialized (e.g. lazy) return type.
pub fn async_handler() -> AsyncResult<()> {
let mut file = File::create("foo.txt")?;
AsyncResult::lazy(move || {
file.write_all(b"Hello, world!")?;
AsyncResult::Ok(())
})
}
Semantically this feels like From::from(E) -> Other<T2, E>
, but use of From
is currently restricted to the existing Result
-equivalent Try
implementation.
I really think NoneError
should have a separate tracking issue. Even if Try
trait never gets stabilized, NoneError
should get stabilized because it makes using ?
on Option
much more ergonomic. Consider this for errors like struct MyCustomSemanthicalError;
or errors implementing Default
. None
could easily be converted into MyCustomSeemanthicalError
via From<NoneError>
.
In working on https://github.com/rust-analyzer/rust-analyzer/, I have met a slightly different papercut from insufficiencies in the ?
operator, particularly when the return type is Result<Option<T>, E>
.
For this type, it it makes sense for ?
to effectively desugar to:
match value? {
None => return Ok(None),
Some(it)=>it,
}
where value
is of type Result<Option<V>, E>
, or:
value?
where value
is Result<V, E>
. I believe that this is possible if the standard libraries implements Try
in the following way for Option<T>
, although I haven't explicitly tested this and I think there may be specialisation issues.
enum NoneError<E> {
None,
Err(E),
}
impl From<T> for NoneError<T> {
fn from(v: T) -> NoneError<T> {
NoneError:Err(v)
}
}
impl<T, E> std::Ops::Try for Result<Option<T>, E> {
type OK = T;
type Error = NoneError<E>;
fn into_result(self) -> Result<Self::OK, Self::Error> {
match self {
Ok(option) => {
if let Some(inner) = option {
Ok(inner)
} else {
Err(NoneError::None)
}
}
Err(error) => {
Err(NoneError::Err(error));
}
}
}
fn from_error(v: Self::Error) -> Self {
match v {
NoneError::Err(error) => Err(error),
None => Some(None),
}
}
fn from_ok(v: Self::OK) -> Self {
Ok(Some(v))
}
}
impl<T> std::Ops::Try for Option<T> {
type OK = T;
type Error = NoneError<!>;
fn into_result(self) -> Result<Self::OK, Self::Error> {
match self {
None => Err(NoneError::None),
Some(v) => Ok(v),
}
}
fn from_error(v: Self::Error) -> Self {
match v {
NoneError::None => Some(None),
_ => unreachable!("Value of type ! obtained"),
}
}
fn from_ok(v: Self::OK) -> Self {
Ok(v)
}
}
When asking @matklad why he couldn't create a custom enum implementing Try
, which would be called Cancellable
in this case, he pointed out that std::ops::Try
is unstable, so it can't be used anyway given that rust-analyzer
(currently) targets stable rust.
Repost from https://github.com/rust-lang/rust/issues/31436#issuecomment-441408288 because I wanted to comment on this, but I think that was the wrong issue:
Essentially, a situation I have is callbacks in a GUI framework - instead of returning an Option
or Result
, they need to return a UpdateScreen
, to tell the framework if the screen needs to be updated or not. Often I don't need logging at all (it's simply not practical to log on every minor error) and simply return a UpdateScreen::DontRedraw
when an error has occurred. However, with the current ?
operator, I have to write this all the time:
let thing = match fs::read(path) {
Ok(o) => o,
Err(_) => return UpdateScreen::DontRedraw,
};
Since I can't convert from a Result::Err
into a UpdateScreen::DontRedraw
via the Try operator, this gets very tedious - often I have simple lookups in hash maps that can fail (which isn't an error) - so often in one callback I have 5 - 10 usages of the ?
operator. Because the above is very verbose to write, my current solution is to impl From<Result<T>> for UpdateScreen
like this, and then use an inner function in the callback like this:
fn callback(data: &mut State) -> UpdateScreen {
fn callback_inner(data: &mut State) -> Option<()> {
let file_contents = fs::read_to_string(data.path).ok()?;
data.loaded_file = Some(file_contents);
Some(())
}
callback_inner(data).into()
}
Since the callback is used as a function pointer, I can't use an -> impl Into<UpdateScreen>
(for some reason, returning an impl
is currently not allowed for function pointers). So the only way for me to use the Try
operator at all is to do the inner-function trick. It would be nice if I could simply do something like this:
impl<T> Try<Result<T>> for UpdateScreen {
fn try(original: Result<T>) -> Try<T, UpdateScreen> {
match original {
Ok(o) => Try::DontReturn(o),
Err(_) => Try::Return(UpdateScreen::DontRedraw),
}
}
}
fn callback(data: &mut State) -> UpdateScreen {
// On any Result::Err, convert to an UpdateScreeen::DontRedraw and return
let file_contents = fs::read_to_string(data.path)?;
data.loaded_file = Some(file_contents);
UpdateScreen::Redraw
}
I am not sure if this would be possible with the current proposal and just wanted to add my use-case for consideration. It would be great if a custom Try operator could support something like this.
@joshtriplett Sorry for taking a while to get back to this. I've put together a working prototype of the proposal at https://github.com/rust-lang/rust/compare/master...scottmcm:try-trait-v2 to be concrete. I hope to try out some more things with it.
@scottmcm Do you have some higher-level explanation of the changes?
@scottmcm FWIW I tried your changes in rayon too:
https://github.com/rayon-rs/rayon/compare/master...cuviper:try-trait-v2
(still using private copies rather than unstable items)
So what is the solution to convert the Option NoneError? It seems that, because, it implements the Try Trait, it'll not compile unless you enable using that particular (unstable?) feature.
So basically, you cannot use the ? operator with Option as far as I'm aware?
@omarabid, the operator is stable for use with Option
or Result
, but you can't use Try
as a generic constraint until it's stable. It's perfectly fine to use ?
on an Option
in a function returning Option
, as you don't have to involve NoneError
at all. You can also return a Result
if you erase types:
use std::fmt::Debug;
pub struct Error(Box<dyn Debug>);
impl<T: Debug + 'static> From<T> for Error {
fn from(error: T) -> Self {
Error(Box::new(error))
}
}
pub fn try_get<T>(slice: &[T], index: usize) -> Result<&T, Error> {
Ok(slice.get(index)?)
}
@scottmcm, your prototype try-trait-v2
fails this example!
If we don't want my example to break, try-trait-v2
will need something like:
#[unstable(feature = "try_trait_v2", issue = "42327")]
impl<T, U, E: From<NoneError>> ops::Bubble<Result<U, E>> for Option<T> {
fn bubble(self) -> ops::ControlFlow<T, Result<U, E>> {
match self {
Some(x) => ops::ControlFlow::Continue(x),
None => ops::ControlFlow::Break(Err(E::from(NoneError))),
}
}
}
What's the current status of this feature?
PR #62606 to document implementing try_fold
for iterators should be reopened once this becomes stable.
Edit: Updated the op with a tracking item for that ~ scottmcm
Are there any plans to replace the Try
trait with any of the alternatives suggested in this thread? The version suggested by @scottmcm seems fine. I want to continue to use the ?
operator with Option
, and I think the Try
trait should be changed to not force Result
semantics on Option
.
Using @scottmcm's alternative would allow us to use ?
with Option
and get rid of NoneError
. I agree with @nikomatsakis (comment) that the Try
trait shouldn't promote the need to "artificially define a type to represent the 'failure' of an Option
".
pub struct Error(Box<dyn std::fmt::Debug>);
impl<T: std::fmt::Debug + 'static> From<T> for Error { fn from(error : T) -> Self { Error(Box::new(error)) } }
type Result<T> = std::result::Result<T, Error>;
Beginner here, I was a bit stubborn in wanting to get ? to automatically type erase both any Error and Option.
After spending way too much time trying to understand why other likely solutions cannot be implemented I've found @cuviper is the closest to what I can get.
Some explanations would have helped, but at least I got to get closely acquainted to some Rust metaprogramming limitations.
So I tried to figure it out and explain in concrete terms.
This thread seems the most likely crossroad where I can hopefully help out anyone like me stumbling on this, feel free to correct :
From<T: StdError> for Error
and a specialized From<NoneError> for Error
conflictstype Error = Box<Debug>
binds Debug which makes From<T:Debug> for Error
conflict with From<T> for T
(reflexive From for idempotence)So because Error cannot implement Debug you might want to have an helper to unwrap into a Result with transitive Debug :
fn from<T>(result : Result<T>) -> std::result::Result<T, Box<dyn std::fmt::Debug>> { match result { Ok(t) => Ok(t), Err(e) => Err(e.0) } }
It cannot be impl From<Result<T>> for StdResult<T>
nor Into<StdResult> for Result<T>
(cannot impl upstream trait for upstream type)
For example, you can use it to get a Debug return for Termination :
fn main() -> std::result::Result<(), Box<dyn std::fmt::Debug>> { from(run()) }
Evil idea : maybe the ?
operator could somehow represent a monadic bind, so
let x: i32 = something?;
rest of function body
becomes
Monad::bind(something, fn(x) {
rest of function body
})
A terrible idea, but it pleases the inner geek in me.
@derekdreery I don't think that would work well with imperative control flow like return
and continue
Keep in mind that the semantics of the ?
operator are already stable. Only the actual Try
trait is unstable, and any changes must preserve the same stable effect for Option
and Result
.
@KrishnaSannasi
@derekdreery I don't think that would work well with imperative control flow like return and continue
I agree with this statement.
@cuviper
Keep in mind that the semantics of the ? operator are already stable. Only the actual Try trait is unstable, and any changes must preserve the same stable effect for Option and Result.
Is this true across epochs as well?
On a more general note, I don't think it's possible to unify concepts like .await
, ?/
early return, Option::map, Result::map, Iterator::map in rust, but understanding that these all share some structure definitely helps me to be a better programmer.
Apologies for being OT.
I'm not sure if an epoch/edition would be allowed to change ?
desugaring, but that would certainly complicate a lot of cross-crate concerns like macros.
My interpretation of the stability guarantees and epochs RFC is that the ?
behavior (sugar or otherwise) could be changed, but its current behavior on Option
/Result
types must stay the same because breaking that would create far more churn than we could ever hope to justify.
What if Option
somehow was a type alias for Result
? I.e. type Option<T> = Result<T, NoValue>
, Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x)
, Option::<T>::None = Result::<T, NoValue>::Err(NoValue)
? That'd take some magic to implement and not realistic in the current language environment, but maybe that worth exploring.
We can't make this change because there are trait impls that rely on Option
an Result
being distinct types.
If we ignore that, then we could take it one step further and even make bool
an alias for Result<True, False>
, where True
and False
are unit types.
Has it been considered to add a Try
impl for unit, ()
? For functions that don't return anything, an early return may still be useful in the case of an error in a non-critical function, such as a logging callback. Or, was unit excluded because it is preferred to never silently ignore errors in any situation?
Or, was unit excluded because it is preferred to never silently ignore errors in any situation?
This. If you want to ignore errors in a non-critical context you should use unwrap
or one of it's variants.
being able to use ?
on something like foo() -> ()
would be quite useful in a conditional compilation context and should strongly be considered. I think that is different from your question though.
you should use unwrap or one of it's variants.
unwrap()
causes a panic, so I wouldn't recommend using it in non-critical contexts. It wouldn't be appropriate in situations where a simple return is desired. One could make an argument that ?
is not totally "silent" because of the explicit, visible use of ?
in the source code. In this way ?
and unwrap()
are analogous for unit functions, just with different return semantics. The only difference I see is that unwrap()
will make visible side-effects (abort the program / print a stacktrace) and ?
would not.
Right now, the ergonomics of early returning in unit functions is considerably worse than in those returning Result
or Option
. Perhaps this state of affairs is desirable because users should use a return type of Result
or Option
in these cases, and this provides incentive for them to change their API. Either way, it may be good to include a discussion of the unit return type in the PR or documentation.
being able to use
?
on something likefoo() -> ()
would be quite useful in a conditional compilation context and should strongly be considered. I think that is different from your question though.
How would this work? Just always evaluate to Ok(()) and be ignored?
Also, Can you give an example of where you'd want to use this?
Yeah the idea was that something like MyCollection::push might, depending on compile time config, have either a Result<(), AllocError> return value or a () return value if the collection is configured to just panic on error. Intermediate code using the collection shouldn't have to think about that, so if it could simply _always_ use ?
even when the return type is ()
it would be handy.
After almost 3 years, is this any closer to being resolved?
@Lokathor that would only work if a return type Result<Result<X,Y>,Z>
or similiar wasn't possible. But it is. Thus not possible.
I don't understand why a layered Result causes problems. Could you please elaborate?
For cross-referencing purposes, an alternative formulation has been proposed on internals by @dureuill:
@Lokathor
Ok, I thought about it more deeply and think I might have found a rather good explanation.
The problem is the interpretation of the return type or weird annotations would clutter the code. It would be possible, but it would make code harder to read. (Precondition: #[debug_result]
does apply your wanted behavoir, and modifies a function to panic in release mode instead of returning an Result::Err(...)
, and unwraps Result::Ok
, but that part is tricky)
#[debug_result]
fn f() -> Result<X, Y>;
#[debug_result]
fn f2() -> Result<Result<A, B>, Y>;
#[debug_result]
fn g() -> Result<X, Y> {
// #[debug_result_spoiled]
let w = f();
// w could have type X or `Result<X,Y>` based on a #[cfg(debug_...)]
// the following line would currently only work half of the time
// we would modify the behavoir of `?` to a no-op if #[cfg(not(debug_...))]
// and `w` was marked as `#[debug_result]`-spoiled
let x = w?;
// but it gets worse; what's with the following:
let y = f2();
let z = y?;
// it has completely different sematics based on a #[cfg(debug_...)],
// but would (currently?) print no warnings or errors at all,
// and the type of z would be differently.
Ok(z)
}
Thus, it would make the code harder to read.
We can't simply modify the behavoir of ?
simply to a no-op,
especially if the return value of a #[debug_result]
is saved in a temporary variable and later "try-propagated" with the ?
operator. It would make the semantics of the ?
really confusing, because it would depend on many "variables", which aren't necessarily known at "writing time" or could be hard to guess via just reading the source code. #[debug_result]
would need to spoil variables which are assigned function values, but it won't work if one function is marked with #[debug_result]
and one isn't, e.g. the following would be a type error.
// f3 is not annotated
fn f3() -> Result<X, Y>;
// later inside of a function:
// z2 needs to be both marked spoiled and non-spoiled -> type error
let z2 = if a() {
f3()
} else {
f()
};
// althrough the following would be possible:
let z2_ = if a() {
f3()?
} else {
f()?
};
A "cleaner" solution might be a DebugResult<T, E>
type that just panics when a certain compile flag is set and it is constructed from an error, but would be otherwise equivalent to Result<T, E>
otherwise. But that would be possible with the current proposal, too, I think.
Reply to @zserik s last post: The macro you described is pointless - charging return type of function based on build configuration will break every match on function result in every possible build configuration except the only one, regardless of way it was done.
@tema3210 I know. I only wanted to point out that @Lokathor's idea wouldn't generally work in practise. The only thing that might partially work is the following, but only with many restrictions, which I don't think is worth it in long-term:
// the result of the fn *must* have Try<Ok=()>
// btw. the macro could be already implemented with a proc_macro today.
#[debug_result]
fn x() -> Result<(), XError> {
/* ..{xbody}.. */
}
// e.g. evaluates roughly to:
//..begin eval
fn x_inner() -> Result<(), XError> {
/* ..{xbody}.. */
}
#[cfg(panic_on_err)]
fn x() {
let _: () = x_inner().unwrap();
}
#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
x_innner()
}
//..end eval
fn y() -> Result<(), YError> {
/* ... */
// #[debug_result] results can't be matched on and can't be assigned to a
// variable, they only can be used together with `?`, which would create
// an asymetry in the type system, but could otherwise work, althrough
// usage would be extremely restricted.
x()?;
// e.g. the following would fail to compile because capturing the result
// is not allowed
if let Err(z) = x() {
// ...
}
}
@zserik Is it possible that it actually takes a form like this?
#[cfg(panic_on_err)]
fn x() -> Result<(), !> {
let _: () = x_inner().unwrap();
}
#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
x_innner()
}
ok, good idea.
I'm not really sure if this is something that needs to be accounted for prior to stabilization but I'm interested in implementing error return traces and I think it involves changes to the Try
trait or at least its provided impl for Result
to get it to work. Eventually I plan on turning this into an RFC but I want to make sure that the try trait isn't stabilized in a way that makes this impossible to add later incase it takes me a while to get around to writing said RFC. The basic idea is this.
You have a trait that you use to pass tracking info into which uses specialization and a default impl for T to maintain backwards compatibility
pub trait Track {
fn track(&mut self, location: &'static Location<'static>);
}
default impl<T> Track for T {
fn track(&mut self, _: &'static Location<'static>) {}
}
And then you modify the Try
implementation for Result
to use track_caller
and pass this information into the inner type,
#[track_caller]
fn from_error(mut v: Self::Error) -> Self {
v.track(Location::caller());
Self::Err(v)
}
And then for types that you want to gather backtraces for you implement Track
#[derive(Debug, Default)]
pub struct ReturnTrace {
frames: Vec<&'static Location<'static>>,
}
impl Track for ReturnTrace {
fn track(&mut self, location: &'static Location<'static>) {
self.frames.push(location);
}
}
Usage ends up looking like this
#![feature(try_blocks)]
use error_return_trace::{MyResult, ReturnTrace};
fn main() {
let trace = match one() {
MyResult::Ok(()) => unreachable!(),
MyResult::Err(trace) => trace,
};
println!("{:?}", trace);
}
fn one() -> MyResult<(), ReturnTrace> {
try { two()? }
}
fn two() -> MyResult<(), ReturnTrace> {
MyResult::Err(ReturnTrace::default())?
}
And the output of a very shitty version of a backtrace looks like this
ReturnTrace { frames: [Location { file: "examples/usage.rs", line: 18, col: 42 }, Location { file: "examples/usage.rs", line: 14, col: 16 }] }
And here is a proof of concept of it in action https://github.com/yaahc/error-return-traces
I thought that only one error type which we can convert to try trait implementer might be insufficient, so, we could provide interface like this:
trait Try{
type Error;
type Ok;
fn into_result(self)->Result<Self::Ok,Self::Error>;
fn from_ok(val: Self::Ok)->Self;
fn from_error<T>(val: T)->Self;
}
Note, there compiler can monomorfize from_error
, avoiding From::from
call, and one can provide method impl for different error types manually, resulting in ability of ?
operator to "unwrap" different types of errors.
fn from_error<T>(val: T)->Self;
As written, the implementer would have to accept _any_ sized T
, unconstrained. If you meant to let this be custom constrained, it would have to be a trait parameter, like Try<T>
. This is similar to the TryBlock
/Bubble<Other>
combination that @scottmcm had in https://github.com/rust-lang/rust/issues/42327#issuecomment-457353299.
As written, the implementer would have to accept _any_ sized
T
, unconstrained. If you meant to let this be custom constrained, it would have to be a trait parameter, likeTry<T>
. This is similar to theTryBlock
/Bubble<Other>
combination that @scottmcm had in #42327 (comment).
I meant that usage should look like this:
trait Try{
//same from above
}
struct Dummy {
a: u8,
}
struct Err1();
struct Err2();
impl Try for Dummy {
type Ok=();
type Error=();
fn into_result(self)->Result<Self::Ok,Self::Error>{
std::result::Result::Ok(())
}
fn from_ok(val: Self::Ok)->Self{
Self{a: 0u8}
}
fn from_error<T>(val: Err1)->Self where T == Err1{
Self{a: 1u8}
}
fn from_error<T>(val: Err2)->Self where T == Err2{
Self{a: 2u8}
}
}
you'd need to split the Try and the TryFromError. I do like that more than the original proposal fwiw but I think it'd need a new RFC.
(and I still think it should've been called "propagate" instead of "try", but I digress)
@tema3210 I think I understand your intent, but that's not valid Rust.
@SoniEx2
you'd need to split the Try and the TryFromError.
Right, that's why I mentioned the TryBlock
/Bubble<Other>
design. We can debate that naming choice, but the idea was that early-return is not always about _errors_, per se. For example, a lot of the internal Iterator
methods are using a custom LoopState
type. For something like find
, it's not an error to find what you're looking for, but we want to stop iteration there.
We can debate that naming choice, but the idea was that early-return is not always about errors, per se.
precisely why I don't like the name "try" and would prefer the name "propagate", because it "propagates" an "early" return :p
(not sure if this makes sense? last time I brought this up the "propagate" thing only made sense to me for some reason and I was never quite able to explain it to others.)
Will this trait be of any help when trying to overwrite the default ?
behavior f.e. to add a log hook f.e. to log debug information (like file/line number)?
Currently it's supported to overwrite stdlib macros, but it seems that the ?
operator doesn't get converted to the try!
macro explicitly. That's unfortunate.
@stevenroose To add support for that solely to the Try
trait, it would require a modification of the Try
trait to include file location information about the location where ?
"happened".
@stevenroose To add support for that solely to the
Try
trait, it would require a modification of theTry
trait to include file location information about the location where?
"happened".
This is not true, #[track_caller] can be used on traits impls even if the trait definition doesn't include the annotation
@stevenroose to answer your question, yes, though if you're interested in printing all the ?
locations an error propogates through you should check out the error return trace comment from above
https://github.com/rust-lang/rust/issues/42327#issuecomment-619218371
I'm not sure if anyone have already mentioned this, shall we impl Try for bool
, maybe Try<Ok=(), Error=FalseError>
?
So that we could do thing like this
fn check_key(v: Vec<A>, key: usize) -> bool {
let x = v.get_mut(key)?; // Option
x.is_valid()?; // bool
x.transform()?; // Result
true
}
Now I have to use return type Option<()>
in most cases where I think ?
could make code much more clear.
I'm not sure if anyone have already mentioned this, shall we
impl Try for bool
, maybeTry<Ok=(), Error=FalseError>
?
This would make Try
behave as the &&
operator on bool
s. As others have pointed out above, there are also use cases for short-circuiting on success, in which case Try
should behave as ||
. Since the &&
and ||
operators aren't much longer to type out, I also don't see much of an advantage to having this implementation.
@calebsander thanks for kindly reply.
That's true for some simple cases, but I don't think it is often the case, especially we could never have assignment statment like let x = v.get_mut(key)?
in expression.
If &&
and ||
would always do the trick, we could as well play with .unwrap_or_else()
, .and_then()
for Option
and Error
cases.
Could you express the flowing code in &&
and ||
?
fn check_key(v: Vec<A>, key: usize) -> bool {
let x = v.get_mut(key)?; // Option
x.not_valid().not()?; // bool
for i in x.iter() {
if i == 1 { return true }
if i == 2 { return false }
debug!("get {}" i);
}
let y = x.transform()?; // Result
y == 1
}
For some condition that true
means failed while false
means success, !expr?
might be confusing, but we could use expr.not()?
to do the trick (note: for ops::Try
, we always have false
for Error
)
That's true for some simple cases, but I don't think it is often the case, especially we could never have assignment statment like
let x = v.get_mut(key)?
in expression.
Well, unless I misunderstand your proposal, just implementing Try<Ok = (), Error = FalseError>
for bool
wouldn't allow this. You would also need to impl From<NoneError> for FalseError
so that the ?
operator could convert None
into false
. (And similarly, if you want to apply ?
to a Result<T, E>
inside a function that returns bool
, you would need to impl From<E> for FalseError
. I think such a blanket implementation would be problematic.) You could also just use some_option().ok_or(false)?
and some_result().map_err(|_| false)?
if you're okay with the return value Result<bool, bool>
(which you can collapse with .unwrap_or_else(|err| err)
).
Leaving issues of converting other Try
errors to bool
aside, the Try
implementation you are proposing for bool
is just the &&
operator. For example, these are equivalent
fn using_try() -> bool {
some_bool()?;
some_bool()?;
some_bool()
}
and
fn using_and() -> bool {
some_bool() &&
some_bool() &&
some_bool()
}
(Admittedly, control flows like if
and loop
which return non-()
are less straightforward to translate.)
For some condition that
true
means failed whilefalse
means success,!expr?
might be confusing, but we could useexpr.not()?
to do the trick (note: forops::Try
, we always havefalse
forError
)
It's not clear to me that false
should represent the Error
case. (For example, Iterator.all()
would want false
to short-circuit, but Iterator::any()
would want true
instead.) As you pointed out, you can get the opposite short-circuiting behavior by inverting the value passed to ?
(and inverting the function's return value as well). But I don't think that results in very readable code. It might make more sense to have separate struct
s And(bool)
and Or(bool)
that implement the two different behaviors.
And similarly, if you want to apply ? to a Result
inside a function that returns bool, you would need to impl From for FalseError. I think such a blanket implementation would be problematic.
No, I don't want to impl From<T> for FalseError
, maybe we could do result.ok()?
It's not clear to me that false should represent the Error case.
I think it somehow natural when we have bool::then
that map true
to Some
.
Forgive me if I missed it - why doesnt NoneError
impl std::error::Error
? This makes it useless for any functions returning Box<dyn Error>
or similar.
Forgive me if I missed it - why doesnt
NoneError
implstd::error::Error
? This makes it useless for any functions returningBox<dyn Error>
or similar.
Not an expert here, but I can see significant problems with trying to come up with a useful error message for "something was None
and you expected it to be Some
" (which is basically what you'd be gaining here -- some diagnostic message). In my experience, it has always made most sense to use the Option::ok_or_else
combinator to use another error type instead, because as the calling code I generally have much better context to give anyways.
I agree with @ErichDonGubler. It's super annoying to be unable to use ?
with Option
, however it's for a good reason, and, as a language user, I think it's in everyone's best interest to handle errors properly. Doing this extra work of binding error context to None
instead of just doing ?
is really, really annoying, but it forces properly communicating errors, which is well worth it.
The only time I'd allow None
be interpreted as an error is at very rough prototyping. I figured, if we add something like a Option::todo_err()
call, that would return Ok
of the internal value, but will panic on None
. It is very counter-intuitive until you analyze the needs of the "rough prototyping" mode of code authoring. It's very similar to Ok(my_option.unwrap())
, but doesn't need an Ok(...)
wrapping. In addition, it explicitly specifies the "todo" nature, thus communicating the intent to remove this code from the production logic, replacing it with a proper error binding.
but will panic on None
Strongly feel that we should just leave this to unwrap
. If we added a todo_err it should return an actual error type, which begs the question of what error type to return.
Also I suspect fn todo_err(self) -> Result<Self, !>
would have the obvious problem of requiring !
to stabilize, which uh, well, Someday.
My usecase is indeed rough prototyping where I don't care about the errors too much. Doesn't the whole NoneError
suffer from these problems you listed? If it is decided that it should exist (which I think is a good thing, at least for prototyping), I believe it should impl Error
as it is named to be one.
Because of the type lacking this impl I resorted to just slapping a .ok_or("error msg")
on it, which works also but is less convenient.
Most helpful comment
What's the current status of this feature?