Open scottmcm opened 3 years ago
It's look like curent FromResidual
is just the From
trait. Why do we need this trait if From is mostly the same ? Unless I miss something they shouldn't have a problem with conflit implementation cause a crate that would have a type that implement Try would by definition own the type.
@Stargateur Unfortunately, that causes conflicts with using ! in the residual type:
#![feature(never_type)]
pub trait From2<T>: Sized {}
// Default impl in the standard library
impl<T> From2<T> for T {}
// Conflicts with the impl above, since T can be !
impl<T> From2<Option<!>> for Option<T> {}
// Conflicts with the impl above, since T can be !, and F can be E
impl<T, F: From2<E>, E> From2<Result<!, E>> for Result<T, F> {}
Marking design concerns because there's still on-going discussion here about trait structure. I don't think there's concerns about the general idea of this (coloured residuals, matching on controlflow, etc), so I don't think there's foundational concerns here, but future work on try
blocks might also want to add a few more bounds on associated types here.
(Basically this is the best S-tracking
fit, since it's implemented but doesn't seem exactly ready-to-stabilize
yet.)
Is it possible to stabilize some subset, like enough to write your own Iterator::try_fold
, while leaving those design concerns open in unstable space? You wouldn't be able to impl Try for ...
until it's finished, but maybe we can use it a little more...
I think Try::{self, Output, from_output}
would be enough, and of course the ?
operator we've long had.
I think
Try::{self, Output, from_output}
would be enough, and of course the?
operator we've long had.
Here's what it looks like for rayon's implementation of try_*
methods to use just that subset:
https://github.com/rayon-rs/rayon/compare/master...cuviper:rayon:unstable-try
Is it possible to stabilize some subset, like enough to write your own Iterator::try_fold, while leaving those design concerns open in unstable space? You wouldn't be able to impl Try for ... until it's finished, but maybe we can use it a little more...
This trait is core to the Rust language, I think it shouldn't be rushed. Specially https://github.com/rust-lang/rust/issues/84277#issuecomment-1066120333 suggest change that are not compatible with your suggestion, we should test it before commit to anything.
I agree it shouldn't be rushed, but I'm trying to give it some forward momentum. This stuff has been around in stable use for a long time -- ?
since 1.13, try_fold
since 1.27 -- so I don't think anyone can accuse it of moving too quickly. Right now we seem to be stuck in a mostly-bikeshed limbo, although there are some functional aspects still being hammered out too. "Perfect is the enemy of good," but I hope an interim goal like "make it possible to write Iterator::try_fold
" will help make progress.
Specially #84277 (comment) suggest change that are not compatible with your suggestion
In that proposal, we would need Try
, Try::wrap
(rename of from_output
), and access to the Branch::Continue
associated type -- I think you can still name that in constraints like Try<Continue = something>
or T: Try, T::Continue: Debug
, without directly naming Branch
. (I know that works with Iterator
subtraits for naming Item
, at least.)
I do like the interim goal!
I'll note that it's actually possible to (indirectly) get to Try::from_output
on stable today:
macro_rules! try_from_output {
($e:expr) => {
std::iter::empty::<std::convert::Infallible>()
.try_fold($e, |_, x| match x {})
}
}
https://play.rust-lang.org/?version=stable&edition=2021&gist=9c3540cb33148314790bd75840aab638
Another option for letting people call it without stabilizing trait details would be to stabilize try{}
blocks -- that's actually how the standard library calls it:
(That was convenient for the v1 → v2 change as it meant the library code didn't need to change, just the try{}
desugaring in the compiler.)
So I think the core thing that'd be needed for an interim bit is just a way to let people write the equivalent of R: Try<Output = B>
. One version of that we could consider is adding something like
// in core::iter
pub trait TryFoldReturnType<Accumulator> = Try<Output = Accumulator>;
so that users can write
fn try_fold<B, F, R>(&mut self, init: B, mut f: F) -> R
where
Self: Sized,
F: FnMut(B, Self::Item) -> R,
R: TryFoldReturnType<B>,
{
and if we change how that's implemented later, great. (We could even deprecate or doc-hide it later, should another option become available, like we did with https://doc.rust-lang.org/std/array/struct.IntoIter.html#method.new.)
Idea written as trait alias for communication simplicity. It'd probably want to be a trait with a blanket impl and an unstable method so it can't be implemented outside core
, or similar, if we wanted to actually do this, because we probably don't want to expose trait aliases to stable consumers just yet.
"make it possible to write
Iterator::try_fold
" will help make progress.
I don't understand what you mean by that, you can use ControlFlow of std that is stable.
Edit: Ah yes the Try trait for the generic.
"Perfect is the enemy of good,"
I agree I just think naming is important, the feature of current Try is fine for me.
"make it possible to write
Iterator::try_fold
" will help make progress.I don't understand what you mean by that, you can use ControlFlow of std that is stable.
I mean for your own custom type implementing Iterator
, it should be possible to write a custom version of the try_fold
method, just like we do for the implementations for Chain
, FlatMap
, etc. At minimum, that means you must be able to match the constraints of that method, R: Try<Output = B>
.
I used Try in a project and there is one thing that I would like but I don't think it's possible. I also don't know if it's needed. My problem is that like iterator I end up have duplicate function, one variant for thing with Try and thing without. My concern is both about user experience and performance. It would be nice that we could impl Try<Residual = Infallible>
for every T
. I think is impossible since we would end with conflict implementation. I end up with something like this:
#![feature(try_trait_v2)]
use std::convert::Infallible;
use std::ops::{ControlFlow, FromResidual, Try};
pub struct NoFail<T>(pub T);
impl<T> FromResidual for NoFail<T> {
fn from_residual(_: Infallible) -> Self {
unreachable!()
}
}
impl<T> Try for NoFail<T> {
type Output = T;
type Residual = Infallible;
fn from_output(output: Self::Output) -> Self {
Self(output)
}
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
ControlFlow::Continue(self.0)
}
}
enum Foo<T, E> {
T(T),
E(E),
}
impl<T, E> FromResidual<Infallible> for Foo<T, E> {
fn from_residual(_: Infallible) -> Self {
unreachable!()
}
}
impl<T, E> FromResidual for Foo<T, E> {
fn from_residual(foo: Foo<Infallible, E>) -> Self {
match foo {
Foo::T(_) => unreachable!(),
Foo::E(e) => Foo::E(e),
}
}
}
impl<T, E> Try for Foo<T, E> {
type Output = T;
type Residual = Foo<Infallible, E>;
fn from_output(output: Self::Output) -> Self {
Foo::T(output)
}
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
match self {
Foo::T(t) => ControlFlow::Continue(t),
Foo::E(e) => ControlFlow::Break(Foo::E(e)),
}
}
}
fn foo<T, U, E>(u: U)
where
U: Try<Output = T>,
Foo<T, E>: FromResidual<U::Residual>,
{
}
fn main() {
foo::<i32, _, ()>(NoFail(42));
foo::<i32, _, ()>(42);
}
From the user point of view that not better than Ok(42)
, from a performance point of view I don't know, I expect Rust is able to optimize a use of Try that can't fail. I would like to know if it's possible to write foo::<i32, _, ()>(42);
.
My second question is: what is the recommandation about duplicate code, should we have two variants for most function or should we only propose the "Try" version ? It's look similar to async problem with "What color is your function ?".
I haven't followed the discussion closely, but I have just run into a situation where I think this would help.
Basically I have an enum Response
of possible responses from a server, including successful responses and error responses. It would be great if I was able to use the ?
operator to easily "short circuit return" when certain other function return errors. For instance, if I get a Err(e)
of some certain type Result<T, E>
, I would like ?
to map that to a certain variant in my response enum and return that (while an Ok(t)
should just continue execution).
A temporary workaround that my team is currently using is the somewhat bizarre type of Result<Response, Response>
. This allows one to use the ?
operator to return a response of a certain variant in case of Err
but also return a response in case of Ok
. However you then have to unwrap_or_else(|e| e)
the result in order to get either the Ok or Err response out.
Again, haven't followed closely but would love if this feature would allow this use case to simply use Response
as a return type rather than Result<Response, Response>
! :)
A temporary workaround that my team is currently using is the somewhat bizarre type of Result<Response, Response>. This allows one to use the ? operator to return a response of a certain variant in case of Err but also return a response in case of Ok. However you then have to unwrap_or_else(|e| e) the result in order to get either the Ok or Err response out.
I would advice if you don't want use nightly to split you Response
into two, one for Ok, one for Err. If you have several ok value or err value use two enums.
enum ResponseOk { Todo };
enum ResponseErr { Todo };
then use Result<ResponseOk, ResponseErr>
that way better than your current approach and will fit nicely when Try trait is stable. If you end up saying "but sometime a response can be either Ok or Err" then the try trait is probably not what you seek (thus I could be wrong here).
Unfortunately the Response type is an enum coming from autogenerated code that is generated by prost, i.e. a Protocol Buffers type definition. So changing it is not so easy.
... then use
Result<ResponseOk, ResponseErr>
that way better than your current approach and will fit nicely when Try trait is stable.
Would the try trait not be able to handle an enum like Response? For instance, imagine this:
enum Response {
Success,
NotFound,
InvalidRequest,
InternalError,
}
With the try trait fully part of the language, would I not be able to write short-circuiting return behavior for this type, using the ?
operator? If not, I'd be kinda worried about the design of the trait as I would think this use case would be the whole point almost. I mean, it's just a Result
with more than two possibilities. I feel like that should be handled by this feature.
I think I'm okay with using the Result<Response, Response>
workaround until the try trait lands (assuming it would work for my use case).
@scottmcm asked me to share my experience with using these traits. I've been writing some Rust for a few years, but not as comfortable with it as with most other languages I know, but maybe that's useful.
My goal was to allow devs to use the ?
operator with any error type to convert to a u32
(more specifically, a UINT
or unsigned int
in C / Win32) as seen here: https://github.com/heaths/msica-rs/blob/db652f4c85123171c9f0f6acf406547655934f13/src/errors.rs#L153-L160. Windows Installer custom actions all have a (C) form of extern "C" UINT FuncName(MSIHANDLE hSession)
. Supported error codes from a custom action are limited, with all other values effectively being mapped to 1603: https://github.com/heaths/msica-rs/blob/db652f4c85123171c9f0f6acf406547655934f13/src/errors.rs#L197-L203.
It wasn't too hard to discover these traits, but it wasn't clear what a "residual" was ("output" was obvious) and needing a newtype for a NonZeroU32 was initially nonobvious; though, it made since once I realized 0 shouldn't result in an Error
so should be an otherwise impossible case. It wasn't until I found https://rust-lang.github.io/rfcs/3058-try-trait-v2.html#implementing-try-for-a-non-generic-type that all the pieces started falling into a place. A good example like that might be good on either (or both) of the traits.
Once implemented FromResidual
for my custom error type, I was still not able to figure out how to return a u32
(effectively) for any error. This is still an ongoing problem. To fulfill my goal, I just want any error to ideally map to one of the handful of values a custom action can return, or at the very least just return 1603 (ERROR_INSTALL_FAILURE
). Though not ideal, at least a workaround exists: since I implement From<std:error::Error>
for my Error
type (effectively like #[thiserror::from]
), one could do:
CString::new("t\0est").map_err(|err| err.into())?
Having to map all errors is less than ideal, though.
~One nit: since this largely seems related to mapping std::result::Result<T, E>
to some arbitrary output, it seems that std::ops::ControlFlow<B, C>
should clip the C
(Continue
) and B
(Break
) type arguments, which seem to better align with <T, E>
; though, this may simply be my own misunderstanding of how this enum is intended to be used.~ (@scottmcm explained why)
That said, both ControlFlow::Break
and ControlFlow::Continue
enums seem very intuitive, as was Try::Output
. It was only Try::Residual
(and, by extension, FromResidual
itself) that were not. Reading through the motivation in the RFC, would something related to "error" or even "break" make more sense? In the RFC, the Try
trait section reads,
The residual that will be returned to the calling code, as an early exit from the normal flow.
So some word more closely related to "error", "break", or even "exit" might help understanding it better.
May I suggest Unexpected
as an alternative to Residual
? After reading through the RFC thoroughly (twice and early on), I think Residual
conveys the meaning best, although English is not my first language. But the rationalization for Residual
is a little hard to follow. Unexpected
might be a better choice there because that is easier to model mentally, and I haven't seen it suggested before.
I think Unexpected has the same problem as Error, where it can make it awkward to use it in places where it is actually expected that it is used. Perhaps "Exceptional" would work better, as it is an exception to the normal flow? But then you might have some baggage from exceptions in other languages.
The RFC cites the Oxford definition as:
a quantity remaining after other things have been subtracted or allowed for
That aligns with my definition, but to me the intuitive interpretation was "extracting the diverging cases and leaving the remaining cases to continue execution", thus outputting (returning) the diverging type and continuing execution with what remained, the residual. If this is specifically for the ?
operator, could one use more explicit and less confusable term like Diverging
?
As a follow-up to my experience above wherein my goal was to allow for any error to effectively return ERROR_INSTALL_FAILURE
(1603), I wanted to support the case where a callee could return a wrapped u32
.
The most obvious way was with specializations as seen here: https://github.com/heaths/msica-rs/pull/22. I'm considering not supporting this case, though, because I think the use cases are small enough not to warrant yet another unstable feature that seems even less likely to stabilize.
Perhaps to facilitate handling specific residuals but falling back to more general one, could core at least support specialization for cases like this? It seems that's the direction being considered, that only core could use specialization because of unsound behaviors in general.
It sounds like both @Victor-N-Suadicani's example and @heaths's example are cases where the desire is for the actual result type to be different than the type that provides the FromResidual
implementation.
I ran into a similar problem when trying to write a type that would allow x?
to panic on failure (effectively becoming sugar for unwrap()
). There needs to be some type Unwrapped<T>
providing FromResidual<E: Display>
so that from_residual
can panic. But this unfortunately means that Unwrapped<T>
needs to be actually exposed in the function signature. Playground link.
In all these cases, effectively there is no distinction between an actual output return and a residual return.
Another use case I can think of is defaulting. Imagine:
struct OrDefault<T>(T);
impl<T: Default> FromResidual<Option<!>> for OrDefault<T> {
fn from_residual(o: Option<!>) -> OrDefault<T> {
Some(Default::default())
}
}
It seems to me that the core disconnect here is that the return type of the function is expected to contain the error handling logic, but these kinds of use cases show that this isn't true. There are multiple plausibly valid ways to convert between, say, Option<!>
and T
, or Result<_, !>
and T
. And the existing syntax has no way to disambiguate between them, so there is no way it can possibly work.
So I think the only reasonable way to do this kind of think is with more syntax, probably an inner try block or some sugar, but I'm not even sure how one would annotate the types to make that work.
We have GATs now, so maybe we can make the design less awkward? Current type for std::array::try_from_fn
doesn’t fill me with joy, and it seems like it could be more readable:
@GoldsteinE I wrote some notes about why I didn't make it a GAT over in Residual
's tracking issue, https://github.com/rust-lang/rust/issues/91285#issuecomment-1308246080
@scottmcm That's reasonable. I feel like current signatures of try_
functions seriously hinder learnability though. Maybe we could have additional trait with blanket impl (as in my playground) or some other kind of sugar?
Additional thought: I think people would probably write their own try_
function more often than new Try
types and it shouldn't require to write this whole incantation every time (even the standard library doesn't do it — there's a private type alias to make it easier).
Sorry to interrupt but a quick question. Can this trait Try
be automatically implemented from Default
?
So some word more closely related to "error", "break", or even "exit" might help understanding it better.
premature, slink
I want to make the argument to unwind most of the residual based try API. I do not know how much can be unwound, and I not know how much should, but here is why I'm starting to lean towards Try
in this form being a mistake.
Let me state that I really appreciate being able to use ?
on Option
and Result
, and I also agree with the desire that this type of functionality is interesting to have for custom types as well. If the only need was to have these traits be some internal abstractions for that operator I would probably not think that much about it.
However some of the APIs that are currently making their way into the nightly builds are really horrible in terms of type definitions. The original Iterator::try_fold
doesn't read all that bad:
fn try_fold<B, F, R>(&mut self, init: B, f: F) -> R
where
Self: Sized,
F: FnMut(B, Self::Item) -> R,
R: Try<Output = B> { ... }
Unfortunately some of the newer APIs require much more involved bounds. A pretty bad example is try_collect
and try_reduce
:
fn try_collect<B>(
&mut self
) -> <<Self::Item as Try>::Residual as Residual<B>>::TryType
where
Self: Sized,
Self::Item: Try,
<Self::Item as Try>::Residual: Residual<B>,
B: FromIterator<<Self::Item as Try>::Output> { ... }
fn try_reduce<F, R>(
&mut self,
f: F
) -> <<R as Try>::Residual as Residual<Option<<R as Try>::Output>>>::TryType
where
Self: Sized,
F: FnMut(Self::Item, Self::Item) -> R,
R: Try<Output = Self::Item>,
<R as Try>::Residual: Residual<Option<Self::Item>> { ... }
As a somewhat seasoned Rust developer it takes me too long to understand what is going on here, and it is even harder to explain to someone not particularly familiar with Rust. The Residual
stuff also makes itself out via some compiler errors (even on stable) and I doubt that users are not at least in parts confused:
1 | fn foo() -> Result<(), ()> {
| -------------------------- this function returns a `Result`
2 | Some(42)?;
| ^ use `.ok_or(...)?` to provide an error compatible with `Result<(), ()>`
|
= help: the trait `FromResidual<Option<Infallible>>` is not implemented for `Result<(), ()>`
= help: the following other types implement trait `FromResidual<R>`:
<Result<T, F> as FromResidual<Result<Infallible, E>>>
<Result<T, F> as FromResidual<Yeet<E>>>
Why do I need to know what a FromResidual
is? The documentation for all of this machinery also does not really do a good job at explaining it either. This is the documentation of the associated Residual
type on Try
(with questions of mine added in italics):
The choice of this type is critical to interconversion (why?). Unlike the
Output
type, which will often be a raw generic type, this type is typically a newtype of some sort to “color” the type so that it’s distinguishable from the residuals of other types. (why? it does not explain the motivation)This is why
Result<T, E>::Residual
is notE
, butResult<Infallible, E>
(again, why? This does not explain the motivation at all). That way it’s distinct fromControlFlow<E>::Residual
(where isControlFlow
all the sudden coming from?), for example, and thus?
onControlFlow
cannot be used in a method returningResult
(None of this is obvious).If you’re making a generic type
Foo<T>
that implementsTry<Output = T>
, then typically you can useFoo<std::convert::Infallible>
as itsResidual
type: that type will have a “hole” (What is a hole? Why do I need one?) in the correct place, and will maintain the “foo-ness” (What is foo-ness?) of the residual so other types need to opt-in to interconversion. (You completely lost me here)
Obviously with some reading on other parts you can piece together all of this, but man this is a complex system for the supposed simple operation of "unwrap success, propagate and convert error on error".
@mitsuhiko I'm not sure if I necessarily agree with the conclusion, but I am definitely quite annoyed with the type signatures too. Another example is OnceCell::get_or_try_init
. Today, its type signature is this:
pub fn get_or_try_init<F, E>(&self, f: F) -> Result<&T, E>
where
F: FnOnce() -> Result<T, E>,
I can pretty much read that almost instantly. It does have generics, but they are succinct and pattern match to extremely widely used patterns.
But in PR #107122, it has been proposed to make this generic to support Try
. The proposed type signature is now:
pub fn get_or_try_init<'a, F, R>(&'a self, f: F) -> <R::Residual as Residual<&'a T>>::TryType
where
F: FnOnce() -> R,
R: Try<Output = T>,
R::Residual: Residual<&'a T>,
My eyes instantly gloss over. I've been using Rust for almost ten years, and I can barely read this.
It is possible that these sorts of signatures will become so ubiquitous that our eyes will learn to pattern match them. But I'm skeptical. There is a lot going on here. There's three traits. Associated types. Weird as
syntax in the type the language. Oodles of symbols. It just does not lend itself to being comprehensible. And even worse, as a member of libs-api, it's really hard for me to even know whether this type signature is correct. Is it covering all the use cases it is intended to correctly? Is it missing any? What are the backcompat hazards? These are all very tricky questions to answer.
Now, the middle road between the complaints I'm lodging here and the Try
trait more generally is that we don't actually have to go through and make every one of these methods generic on Try
. The problem with that sort of road is that it's like running up an ever increasing hill.
I'm new to rust and have been very much enjoying what I've seen so far. I love the ? operator. I was quite disappointed that I couldn't use it on Option. Then I stumbled across this issue :)
Can someone summarize for me and others that are new to the language like me why adding ? support to Option is so challenging? What are the edge cases that we are struggling to overcome?
From the perspective of a new and naive user, it seems like converting to Error should be simple enough. We already have ok_or and ok_or_else. It seems like we could just bundle that with ?.
(Again I'm naive and new :). I'm sure it's more complex than that)
The new API generalization wordiness all comes from ops::Residual
moreso than Try
directly, though the two are of course entwined. It's also worth noting that there's an alias used that's not exposed in rustdoc which makes the signatures a bit clearer and a bit less cluttered:
fn try_collect<B>(&mut self) -> ChangeOutputType<Self::Item, B>
where
Self: Sized,
<Self as Iterator>::Item: Try,
<<Self as Iterator>::Item as Try>::Residual: Residual<B>,
B: FromIterator<<Self::Item as Try>::Output>,
;
Just for interest, here's how it could look with associated type bounds:
fn try_collect<B>(&mut self) -> ChangeOutputType<Self::Item, B>
where
Self: Sized,
Self::Item: Try<Residual: Residual<B>>,
B: FromIterator<Self::Item::Output>,
It's still dense, but a lot less noisy; in my opinion it's noticeably clearer what the signature is saying. (Assumes the <Self::Item as Try>::Output
associated item disambiguation could be made unnecessary.)
The difficulty fundamentally comes from wanting to rebind to the same try "kind" (i.e. Result
versus Option
). Without trying so hard to prevent this, the signature can become a lot easier to look at, even without associated type bounds:
fn try_collect<B, R>(&mut self) -> R
where
Self: Sized,
Self::Item: Try,
R: Try<Output = B, Residual = Self::Item::Residual>,
B: FromIterator<Self::Item::Output>,
;
The problem is that this kills type inference, for a related reason to why try
blocks are currently nearly unusable without extra type annotation: you can't derive the output "try kind" uniquely anymore. Rust isn't really able to talk about Result
rather than specifically Result<T, E>
; that's where the residual comes from, as we can talk about Result<!, E>
. (try
blocks additionally suffer from error conversion meaning the error type isn't known uniquely until further constrained.)
Working backwards from the error, what might we ideally want it to look like? Perhaps:
1 | fn foo() -> Result<(), ()> {
| -------------------------- this function returns a `Result`
2 | Some(42)?;
| ^ use `.ok_or(...)?` to provide an error compatible with `Result<(), ()>`
|
= help: the trait `Try<Option<_>>` is not implemented for `Result<(), ()>`
(FWIW, I think the offering of other implementations here is just wrong. It depends on what you consider the polarity of the error to be: that the return type can't receive )
That could give us the trait shape of:
pub trait Try<R> {
type Output;
fn from_output(output: Self::Output) -> Self;
fn branch(self) -> ControlFlow<R, Self::Output>;
}
I chose R
to signify the "R
eturn" type of the try
unit, but if it weren't for the overwhelming std style of single-letter type generics, I'd probably call it Break
. I think this supports all of the current cases,
but it doesn't really help at all with the rebind case or try
, and in fact makes it significantly worse, since now you need to know the "try output" type in order to determine what the output type of e?
is.
Knowing the output type directly from e?
is a major reason the residual type is associated with. The counterparty is thus to embrace the rebind and throw GATs at the problem, e.g.
pub trait Try {
type Output;
type Break<T>;
fn from_output(output: Self::Output) -> Self;
fn branch<T>(self) -> ControlFlow<Self::Break<T>, Self::Output>;
}
but now we've overcorrected and thrown the whole ball of GAT complexities into the mix, plus heavily cut back on actually useful expressivity:
From
when using ?
on Result
isn't possible anymore.?
at all. The Option <-> Result
one that accidentally resulted from the v1 Try
has been removed, but Poll<Result> <-> Result
and Poll<Option<Result>> <-> Result
conversations were deliberately added.HResult
example from the RFC with restricted Output
type domains or which just aren't generic over Output
type in the first place no longer fit in with the use of GAT here. These same reasons apply to why ops::Residual
is generic rather than ops::Residual::TryType
, despite R::Residual::Try<&'a T>
reading much better than <R::Residual as Residual<&'a T>>::Try
.With the hindsight of the difficulties try kind conversion and error conversion bring, maybe it would have been better if the Poll
kind adjustment was a transpose
method and error conversion required .map_err(Into::into)
. However, error conversion is a huge part of what makes ?
convenient to use (even though uses often end up with a .context()
style conversion attached anyway), and kind conversion has its motivating examples as well (e.g. a manually packed version of Result
for specific T, E
).
The last option is to go back towards v1 (using the do yeet
terminology):
pub trait Try {
type Output;
type Yeet: Yeet<Self>;
fn from_output(output: Self::Output) -> Self;
fn branch(self) -> ControlFlow<Self::Yeet, Self::Output>;
}
impl Try<Output = T, Yeet = E> for Result<T, E>;
and perhaps it's okay that this results in being able to ?
between Option
and Result<_, ()>
since you can do yeet ()
to both, but this still does nothing for the rebind case that ops::Residual
exists for.
So I don't think it's possible to cover try
without three traits unless you're willing to drop functionality. You need Try
to split the control flow, FromResidual
to catch the broken value into a different type, and Residual
to be able to rebind Try
types to different output types. Only the functionality of the third isn't stable already, and it's the most foreign concept to anything currently stable (as it exists only to provide a type level mapping rather than any functionality of the impl type).
I'm quite generic-pilled, and to me the current shape seems fine, although the naming and documentation could certainly be improved. I don't have any good naming suggestions, though. My only remaining relevant observation is that the Residual
type/name does feel intrinsically linked to do yeet
, but the two are distinct concepts, since you do yeet
the data carried by the residual.
Perhaps the answer is to (improve the docs for Try
and) drop ops::Residual
and any dependent functionality, either solving its problem space with keyword generics/effects or not at all.
Besides the type bounds I think part of the problem here is that "residual" is also an incommon English word that doesn't mean much to non native speakers. I would not be surprised if you say "residual" the only association that people have is the word "waste". But I don't think that is the biggest issue here.
and perhaps it's okay that this results in being able to
?
betweenOption
andResult<_, ()>
since you can do yeet () to both, […]
On it's own I think being able to go from Option
to Result<_, ()>
both makes sense and is quite convenient. If that's the main restriction we are dealing with here, I would not have assumed that this needs to be resolved.
but this still does nothing for the rebind case that
ops::Residual
exists for.
Which I'm not sure what this is trying to address so I cannot give a comment here to it. My observation is that as an outside observer, I do not understand anything what's going on here and I hope I do not have to do a PHD in types to use some of those APIs.
I think the right way to go about this is:
Option
and Result
I can come up with a bunch of quite trivial ways to go about 1, I don't think I could go about 2.
\2. is already done and stable with Poll
, though, so not doing it isn't an option.
Rebinding refers to the use in e.g. try_collect
, taking some try type (e.g. Result<T, E>
) and a new output type (e.g. Vec<T>
) to determine a different try type (e.g. Result<Vec<T>, E>
).
ops::Residual
is only used for rebinding. The FromResidual
mechanism isn't all that complicated in actuality, it's just somewhat difficult to explain. It's no more complicated than other uses of newtype wrappers, just obscured a bit by the use of !
rather than fresh types.
That’s why I’m wondering how much could even be unwound. I’m not convinced that there is no path to undo the damage caused by Poll
if there was a desire to do so. The treatment of into_iter
on arrays comes to mind which had a creative solution that allowed a change that was assumed to be not possible.
One case of concern to me is that this deeply bakes in what feels like an escape hatch for Result
, namely the possibility of automatic error conversion via From
, which makes type conversions and re-bind prevention imo harder to reason about. I would prefer it if this aspect were realized in a way that doesn't hamper type inference as much (or perhaps make it completely optional via edition and put that abstraction into a completely separate trait). That is, this basically folds multiple conceptually distinct conversions together, but the abstraction doesn't seem particularly fitting (the problems with type inference also make that difficult to use in most cases that involve From
, and to me it seems that that should be solved separately if possible (perhaps via syntactic sugar for From::from
or such))
Do we need to use the same set of traits for governing the built-in language features (?
, try
, hypothetical yeet
) and for expressing abstractions in library code (try_collect()
and company)? If we don't do that, could it reduce the complexity/requirements load on either or both? I'm reminded of the way we have Deref
, AsRef
, and Borrow
as well, which has some downsides, but potentially some upsides also.
(That is, it would carry the tradeoff of further proliferating traits; but you would only have to interact with the ones relevant to your current use case. This was meant as an actual question for my own edification, not a thinly veiled pointed suggestion.)
If I may summarize some of the discussion here, in part for my edification, it sounds like there are several distinct issues being discussed concurrently:
1. Use of the word "residual" in the API
The naming of "residual" for "the thing that gets passed up to the caller with the ?
operator." This naming is shown in the Residual
associated type on Try
, and in the FromResidual
trait. The central concern about the name is that it may not be immediately clear, especially to non-native English speakers, what the "residual" of a Try
type is. A number of alternative names have been proposed, but there are concerns about implying "error"-ness or "unexpected"-ness, because those implications may not make sense for every context. Hence the relatively-neutral-but-unusual word "residual."
2. The impact of having both the Try
and FromResidual
traits on the complexity of trait bounds.
Several examples have been given in the thread of new APIs involving these traits which see their signatures become increasingly complex as they incorporate these experimental traits. In particular, additional work is needed to tie the <T as Try>::Residual
to the R
parameter in the FromResidual
trait, to make sure they're the same when needed. It is possible that permitting associated trait bound syntactic sugar would improve this situation, but this may not be considered sufficient.
3. A question of the value of splitting Try
and FromResidual
in the first place.
Obviously, this was central to the changes from version 1 of the Try
trait proposal to version 2. The value of having FromResidual
as a super-trait of Try
is that any Try
-able type can support handling the use of the ?
operator on other types, so long that there's an impl FromResidual<SomeOtherType> for MyType
. This flexibility can be nice! However, the split between these two traits seems so far to have confused a number of folks.
4. A question over whether the current design can be changed, given its use throughout the standard library.
This API has already impacted the standard library, particularly through various try_*
functions, and the FromResidual
implementations for Poll
. There's a question of how much here can be rolled back. I don't have a clear sense of the constraints / complexity of this task.
5. Concerns about the Infallible
stuff appearing in the relevant impls.
As I take it, the use of Infallible
in different places has to do with Rust's inability to reason about higher-kinded types as first class citizens. Infallible
isn't super common in Rust code today, so people may be surprised by or unfamiliar with it.
6. Concerns about also teaching the helper ControlFlow
enum.
The method Try::branch
returns a ControlFlow
, which means documentation for Try
has to teach this type and what it means. Not impossible, but not done super well as-of-yet.
7. Concerns about how well this design plays with Rust's type inference.
Bad-type-inference errors are generally very frustrating to deal with, because (especially without good error messages) you can easily end up in a space where you feel like you're adding random hints to the compiler until it tells you the types are correct. I take it this current API has inference issues, although I don't understand the technical specifics yet.
If I've missed any big topics under discussion which are currently blocking progress on v2 of the Try
trait, let me know.
I do notice that a lot of these are education questions, about how easy it will be for people to understand this new API. This touches on documentation, error messages, and the terminology itself.
There are distinct questions about implementation, which basically amount to questions of complexity of bounds and difficulty of correct type inference.
All of that said, I'd like to throw my hat in as someone interested in helping. I have a crate called woah
(woah::Result
's Try
impl) which I am keeping pre-1.0 in part because it really needs the Try
trait available to be usable, and I don't want to stabilize with a nightly-only unstable trait being so central to the design.
I'm particularly interested in helping around the documentation / education parts of things. Let me know if there's a good place to start.
A number of alternative names have been proposed, but there are concerns about implying "error"-ness or "unexpected"-ness, because those implications may not make sense for every context.
Seeing this randomly gave me an idea: How about ‘Alternate’?
It seems to fit the ‘neutral’ requirement, not making a particular value judgement beyond ‘different’, and seems to work well in prose, i.e. ‘the alternate path is taken’
I'd say it's also more intuitive for both native and non-native speakers, compared to a slightly odd interpretation of a relatively obscure word
(NOT A CONTRIBUTION)
A small change that I think would make this a lot easier to intuit would be to remove the default value for the generic parameter on FromResidual
and change the supertrait bound on Try
to Try: FromResidual<Self::Residual>
. Some notes:
FromResidual<R = <Self as Try>::Residual>
. I find it hard to even figure out that what I'm looking at is a generic trait with a default value for the parameter, and not something like a supertrait bound.FromResidual
isn't really meant to be used anywhere, rather than Try
, and so getting the default parameter doesn't seem to have much benefit.Frankly, when I read that you had modified the Try
trait to add a super bound that looks like FromResidual<R = <Self as Try>::Residual>
, my immediate reaction was very negative. When I read through the RFC, I was more convinced that the actual structure of the interfaces has unique utility. But this specific quirk of it I think doesn't carry the weight of the cognitive load it adds to the interface.
I also think that "residual" is an unclear name and this isn't an area where Rust needs any more unique terms of art, but as a rule I now try to keep my comments on the lowest rung of Wadler's ladder possible so I won't push that further.
As another note, I think the desire the "generalize" APIs that currently involve closures returning Result
to support any try type is a mistake in itself. I don't think there's a way to not get highly generic, inscrutable type signatures (regardless of the decision on the definition of Try
), and I don't think there's enough utility in doing that to justify the cognitive overhead of those signatures (since Try
types should have a pretty straightforward conversion to Result
if you really need to use some other Try
type in that case). PRs like #107122 should be closed as undesired.
Okay, it does seem like the question of whether the flexibility afforded by FromResidual
is worth it is the most fundamental question. Improving naming or documentation around a feature are secondary to deciding if the feature should remain.
To help inform that discussion, I'd like to work on answering some questions:
FromResidual
??
and Try
appear to use non-Result
types?FromResidual
?Each of these can at least attempt to be answered by surveying available Rust codebases, something which I believe has already been done at least in part by others. I'd like to go through the discussions to identify any samples already taken, and likely add more samples myself, to help everyone involved have more concrete reference points for the benefits, additional complexity, and possible demand for this feature.
In practice, how much usage of
?
andTry
appear to use non-Result
types?
Asking this question for unstable functionality rarely gives useful answers, because for most use cases, using a stable-compatible workaround is preferable to using unstable functionality.
There are five types which can be stably used with ?
: Result<_, _>
, Option<_>
, ControlFlow<_, _>
, Poll<Result<_, _>>
, and Poll<Option<_>>
.
All Try
types will look "Result
-ish," because the try operation is monadic, and Result
is a canonical monad.
What specific additional generic powers become available in the presence of
FromResidual
?
The primary purpose of FromResidual
is that it defines what types you can use ?
on in a function which is returning Self
.
The biggest incidental complexity of the residual system imho is the fact that there isn't a one-to-one mapping between Try
and residual. The residual (definition "the part left behind") is supposed to represent the try type without the output case, so it's unfortunate that custom impls are getting implicitly nudged into just using Result<!, E>
or Option<!>
instead of actually encoding their type's residual.
In implementing FromResidual<Result<!, E>>
for a type, you are stating that you should be able to ?
a Result<_, E>
in functions returning that type. That this meaning is obscured behind an intermediate is unfortunate.
Something along the lines of FromResidual
needs to exist, though, because it's allowed to apply ?
to a nonequal type, both in the simple "stem preserving" case of Result<T, E> -> Result<T, impl From<E>>
and in the "stem changing" case of Poll
's transparent Try
impls.
(Although the latter is somewhat controversial in retrospect and Poll::ready
exists unstably to "fix" it, it's stable and changing it is essentially[^1] a nonstarter.)
[^1]: Since ?
is essentially language functionality while the library plumbing remains unstable, the behavior of ?
could theoretically be changed over an edition to use a different trait which is implemented differently. But while technically possible, it's a horrible thing to change in practice.
How much more complex, in real world codebases, do type signatures become when incorporating the additional flexibility of
FromResidual
?
Frankly, FromResidual
probably shouldn't show up generic signatures. The only time it makes any potential sense is with a generic return type (to communicate the potential failures the return type needs to accept), and a nongeneric return type of Result
accomplishes this just as well, while not having the usability issues inherent to generic return types. And even then you still need to have some additional bound enabling the function to construct a success output.
Basically, being generic over FromResidual
is like being generic over From
— it doesn't make sense in 99% of cases to be generic in that direction. That direction is a trait because it makes the usefully generic direction of Into
easier to express interconversions for.
The trait which would actually be appropriate for generic return types is Residual
, which maps back from a residual to canonical Try
type (that one-to-one correspondence), allowing you to map over success type (e.g. Result<T, E> -> Result<U, E>
). This is the axis that try_*
functionality would potentially be generic over, e.g. collecting impl Iterator<Result<T, E>> -> Result<impl FromIterator<T>, E>
.
And Residual
does result in incidental complexity in expressing that genericism; what you want to say for Q: Try
is roughly just <Q as Try>::ButWithOutput<T>
, but instead it needs to be spelled as <<Q as Try>::Residual as Residual<T>>::TryType
.
Expressing maximally flexible monads in Rust is known to be a hard problem. And nearly all of that flexibility is actually used for individually reasonable applications.
I think I agree with boats here in that being generic in this way probably isn't necessary, and that try_*
functionality can stick to being defined in terms of the canonical try type, Result
, for much more legible signatures and little/no cost to functionality, just generality, since callers would need to reify between their custom Try
type and Result
.
As an actual example, see Iterator::try_reduce
:
// today's plumbing
type RebindTryOutput<T, O> = <<T as Try>::Residual as Residual<O>>::TryType;
fn Iterator::try_reduce<F, R>(&mut self, f: F) -> RebindTryOutput<R, Option<R::Output>>
where
F: FnMut(Self::Item, Self::Item) -> R,
R: Try<Output = Self::Item>,
R::Residual: Residual<Option<R::Output>>,
;
// "ideal" spelling of today's plumbing
fn Iterator::try_reduce<F, R>(&mut self, f: F) -> R::RebindTryOutput<Option<Self::Item>>
where
F: FnMut(Self::Item, Self::Item) -> R,
R: Try<Output = Self::Item>,
R::Residual: Residual<Option<Self::Item>>,
;
// just Result
fn Iterator::try_reduce<F, E>(&mut self, f: F) -> Result<Option<Self::Item>, E>
where
F: FnMut(Self::Item, Self::Item) -> Result<Self::Item, E>,
;
Some kind of similar functionality to Residual
probably still needs to exist, though, to make try
blocks somewhat functional without mandatory type hinting.
In practice, how much usage of ? and Try appear to use non-Result types?
I'm working on a project since 1 years that fully use all features of Try trait v2 so I hope the FromResiduel feature will not go away... I try to work on the doc to release it one day. I guess it will be a good example for this thread.
Using Result
in the try_*
funcions means giving up on this part of the motivation from the RFC:
Using the "error" terminology is a poor fit for other potential implementations of the trait.
At least in the scope of these functions. In some cases, such as OnceCell::get_or_try_init
, that's probably not terrible, since if failing to init the cell, would generally be considered an error. But for something like try_fold
, you are more likely to have situations where breaking is actually a success, not on error.
Maybe we can use "exception" rather than error. As the try "triggering" would generally be the exceptional case and the common case would be continuing the straight-line code.
(NOT A CONTRIBUTION)
The trait which would actually be appropriate for generic return types is Residual, which maps back from a residual to canonical Try type (that one-to-one correspondence), allowing you to map over success type (e.g. Result<T, E> -> Result<U, E>). This is the axis that try_* functionality would potentially be generic over, e.g. collecting impl Iterator<Result<T, E>> -> Result<impl FromIterator
, E>.
I think this is the part that people really are concerned about. Frankly, I think Residual
should be removed, and all of these. highly generic APIs that cannot be written without it should be removed as well.
I'm going to be very honest about what I think, because I think it is also the opinion of many other prominent community members who are not involved in the libs/lang team clique and have been critical on issues like this or on Twitter. It seems to me that the product design group is trapped in some sort of code golf death spiral where one side creates extremely generic convenience APIs (i.e. try_collect
) and the other side proposes whole new axes of abstraction to hide some of that highly generic type signature behind a language feature (i.e. keyword generics), while most users do not have any pressing need for these APIs or abstractions, which are at best "nice to have" and cannot justify the huge cognitive overhead they bring to the language and standard library.
The Try
feature should be brought back to its original scope: making try
and ?
work with multiple types. The actual content of the RFC for Try v2 is fine, in my opinion, with the exception of the one little change I proposed in my previous comment. The addition of the Residual
trait and the fake-monad type hacking by way of double projection from Try
to Residual
and back to Try
with a swapped enclosed type needs to be dropped.
the fake-monad type hacking by way of double projection
yeah, I'd rather have proper functor, monad interfaces in Rust than such hackery (and I'd really appreciate that solely because dealing with ASTs which are extended/modified through various stages of progressing lead to that sort-of naturally, and being unable to abstract over multiple of them makes it harder than necessary to write generic AST mangling libraries imo).
Out of curiosity, @withoutboats, modulo exact naming,
Would you consider the use of monadic reprojection be more palatable if it were spelled -> R::Rebind<O>
instead of the double projection (-> <R::Residual as Residual<O>>::TryType
)? Presuming that it was just an alias to the use of Residual
, that it showed up in docs as the alias, and that the language feature exists to associate a type alias to a trait without letting it be overridden.
What if it was a typical generic associated type of the Try
trait instead of the double projection, but there was an associated trait alias used to bound the generic type[^1]?
[^1]: The ability to bound this is why we currently have <R::Residual as Residual<O>>::TryType
and not R::Residual::TryType<O>
. (...And that doing so would currently also cause an ambiguous associated type error, but IIUC that's considered a bug/fixable limitation.) The bound is necessary to support e.g. ErrnoResult
.
I ask because I'm legitimately interested in whether it's the use of a generic monadic rebind at all (given that it's not strictly necessary when alternative Try
types can temporarily reify into Result
) which is considered unnecessary cognitive overhead, or if it's just the use of a double projection to accomplish it. (The former is the inherent complexity of making the API generic over this axis; the latter is incidental complexity of how the result is achieved.)
I ask this now prompted by learning of an open draft experiment utilizing a similar double projection scheme to permit allocator-generic collections to do -> A::ErrorHandling::Result<T, E>
to select between -> Result<T, E>
and -> T
based on whether the allocator is (statically) in fallible or infallible mode. This permits the unification of the e.g. reserve
/try_reserve
split, but at the cost of any potentially allocating method which would have had a try_*
version having the more complex signature.
I do agree that doubled type projection is essentially unprecedented in stable std function signatures. The use of a type alias for clarity in the definition of functions doing so is telling, and at an absolute minimum imo the alias should be made public so it will show up in the API docs.
I doubted it for a moment, but the use of a <Type as Trait>::Type
return type in the API docs of stable functions is precedented, at least. Namely, Option::as_deref
is documented as having the signature (&self) -> Option<&<T as Deref>::Target> where T: Deref
[^2].
[^2]: This is despite it being implemented with -> Option<&T::Target>
; I presume rustdoc doesn't bother figuring out whether a projection can be treated as unambiguous. Especially since when such a type projection would be ambiguous in rustdoc is a complicated and distinct question than it being unambiguous in the source code.
(NOT A CONTRIBUTION)
Would you consider the use of monadic reprojection be more palatable if it were spelled
-> R::Rebind<O>
instead of the double projection
Both the action of rebinding the wrapped type and the use of double projection to do so add cognitive overhead to the API. A GAT certainly seems simpler than a double projection, though when you start talking about "associated trait aliases" I get very suspicious. I think either aspect of this alone is complex enough to warrant a lot of caution, and require very compelling motivation. I don't see any of the enabled APIs as particularly well motivated, for exactly the reason we've discussed - you can just map things to Result
if you really want, though even with Result
I doubt the utility of all these try_
variants at all.
I ask this now prompted by learning of an open https://github.com/rust-lang/rust/pull/111970 utilizing a similar double projection scheme to permit allocator-generic collections
My immediate impression is also to be dubious of these fallible allocator APIs. I think allocators is another area where the working group seems to have gone down a rabbit hole and lost touch with how much complexity they're adding to the APIs. I realize there are motivations, but these motivations need to be balanced against how difficult they make std to understand. Maybe I underestimate how compelling the motivations are for that case, I haven't followed the work closely.
Overall, I think most people who work on Rust open source are very disconnected from how most non-hobbyist users experience Rust. They don't have time to nerd out about the language, so the model of the type system in their head is a bit wrong or incomplete, and they can easily be frustrated or confused or led astray by the kinds of type wizardry the online Rust community embraces without question. Rust has always been trying to bring a great type system "to the masses" (in total opposition to Rob Pike's statement that users "are not capable of understanding a brilliant language"), but this means recognizing when the cognitive overhead complex types are adding is not justified by the utility they bring to the API. This is especially true when that utility is basically just omitting a type conversion, as in a lot of these Try cases.
Of course, this sort of thing is fine in third party crates, where it can be proved out and experimented with and in libraries targeted at a certain kind of user can be effective for that user in achieving their goals. But the standard library should be a bulwark against this sort of programming, because the standard library is the lowest common denominator for all users.
Namely, Option::as_deref is documented as having the signature
(&self) -> Option<&<T as Deref>::Target> where T: Deref
This seems like sloppiness to me, as_deref
should be in a separate impl block with the where clause on the impl block, rather than the where clause on the method in a normal impl block. This should solve the problem with rustdoc (though I also think this is a poor showing from rustdoc as well). Not sure if this is a technically breaking change to make, though.
I don't have too much to say regarding the actual design of the trait, although I think it's apt to bring up this argument I made in the original RFC: https://github.com/rust-lang/rfcs/pull/3058#issuecomment-797873462
The term "residual", despite being weird English, is unique enough that it could become synonymous with what it's being used for here. There are lots of terms that aren't used in Normal English that are used in Computer English (for example, verbose) and I think that adding another, although weird, is not the worst.
Essentially, however weird the idea of a "residual" is, the term is unique enough that its current meaning in Rust can be learned in isolation.
Folks have demonstrated that they have made use of the FromResidual
trait in their own implementations and I think that it would be nice to retain this in the stable implementation. I do however sympathise greatly with the bounds required for methods that do use the Try
trait, requiring both Try
and FromResidual
in the bounds.
I think that FromResidual
should probably exist as a relatively "niche" feature that does not need to be understood to use the Try
trait in most cases. It should be required to implement the Try
trait, but that's a higher bar IMHO, and most people just want to make try_*
methods that work without having to understand the whole system.
I feel like maybe more effort should be put into ensuring that the Try
trait is easy to use, without sacrificing the flexibility of FromResidual
.
@CAD97 just to note: I consider such double projection, especially as in applications of them also trait bounds on them can appear in downstream or implementation code, to be an implementation detail that imo shouldn't be necessary, i.e. should be abstracted by the language, leading to a cleaner interface.
re: the concern of "niche" or
Overall, I think most people who work on Rust open source are very disconnected from how most non-hobbyist users experience Rust. They don't have time to nerd out about the language, so the model of the type system in their head is a bit wrong or incomplete, and they can easily be frustrated or confused or led astray by the kinds of type wizardry the online Rust community embraces without question.
I believe that it shouldn't be necessary for most normal users to deal with the intricate parts of the API directly at all in most use cases, and if they do, it should be minimally invasive. A long type signature with a large amount of trait bounds and such that can't be abstracted away (except via macros, which makes the docs unhelpful) is a strong anti-pattern, and imo thus warrants a more fundamental solution, e.g. making all monads easier to express in rust instead of abstracting over it via a combination of multiple interlocked traits with potentially confusing semantics, and also potentially harder to read error messages in case of failures of type inference and such.
Such interfaces are not only annoying to write/copy-paste and debug, they pose a mental burden, make it harder to present users with good error messages (because the compiler doesn't see the actual abstraction but a workaround around the lack of abstraction, mostly), and might also make type inference/checking unnecessarily harder (and e.g. "simple guesses" by the compiler in case of errors also get much harder, both from the "implement this in the compiler" and "make it fast and maintainable" (also in regard to similar patterns which might evolve in third-party crates, etc.).
also, another idea would be to introduce some kind of "tagged ControlFlow
", and just use that everywhere (especially try_
functions), forgoing the most difficult parts of this, while still allowing guided interconversions (mediated by some kind of type-level tags (ZSTs))
I think that FromResidual should probably exist as a relatively "niche" feature that does not need to be understood to use the Try trait in most cases.
That is already kinda the case. If you're using a Try
type, you don't need to care about FromResidual
. If you're writing one, then you will be forced to write exactly one impl of it, which is only fair.
Then again, if you're using one of the ready-made functions, you won't need to care much about the traits beyond the documentation.
However, unless I'm reading the thread wrong, most of the discussion right now seems to be about the Residual
trait, as opposed to the Try
& FromResidual
traits. The relevant tracking issue is #91285, not here.
For what it's worth, I think things like try_collect
are useful (and will be even more so when try trait stabilises and Result
isn't the only result-like type anymore). That said, double projection does make all the relevant type signatures headache-inducing. So maybe something like this would work better?
trait TryWith<O>: Try {
type TryWith: Try<Output = O, Residual = Self::Residual>;
}
I'd be cautious of GATs here, since they'd forbid any impl blocks from adding additional bounds on the projected Output, though I haven't thought about it much so I'll leave it at that.
But again, wrong tracking issue.
Also I'll echo @withoutboats on removing the default for R
from FromResidual<R>
for readability reasons. Appart from that I think the try trait design (which does not include the Residual
trait) is fine.
As somebody who would like to see this stabilized, what can I do to help? Is there anything I can do to help push this forward?
This is a tracking issue for the RFC "
try_trait_v2
: A new design for the?
desugaring" (rust-lang/rfcs#3058). The feature gate for the issue is#![feature(try_trait_v2)]
.This obviates https://github.com/rust-lang/rfcs/pull/1859, tracked in https://github.com/rust-lang/rust/issues/42327.
About tracking issues
Tracking issues are used to record the overall progress of implementation. They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions. A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature. Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.
Steps
Delete the old way after a bootstrap updatehttps://github.com/rust-lang/rust/pull/88223FromResidual
but notTry
FromResidual
better (Issue https://github.com/rust-lang/rust/issues/85454)Infallible
are either fine that way or have been replaced by!
Iterator::try_fold
fold
be implemented in terms oftry_fold
, so that both don't need to be overridden.)Unresolved Questions
From RFC:
Try
use in the associated types/traits? Output+residual, continue+break, or something else entirely?From experience in nightly:
FromResidual
from a type that's never actually produced as a residual (https://github.com/SergioBenitez/Rocket/pull/1645). But that would add more friction for cases not using theFoo<!>
pattern, so may not be worth it.type Residual;
totype Residual: Residual<Self::Output>;
.Implementation history
try_trait
fromstdarch
, https://github.com/rust-lang/stdarch/pull/1142