Open scottmcm opened 3 years ago
What still needs to be done?
From what I understand the main blocking points are:
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>,
Alright, response:
Iterator
and Try
, and the complex types just stem from the same fully-qualified syntax headaches applied to both traits in sequence.Thank you for the answer; I look forward to pushing this through :smile:
@scottmcm Would you be willing to update your checklist to reflect newer changes?
I'd like to raise a concern about the default type parameter on FromResidual
(that is, <Self as Try>::Residual
). I was doing some experimentation today, and it turns out this implementation is considered conflicting
struct Test;
impl<T> FromResidual<Test> for Option<T> {}
With some help from someone on discord (@zachs18), we've been able to determine that this seems to be a consequence of there being an impl<T> FromResidual<<Self as Try>::Residual> for Option<T>
rather than a "handwritten" impl<T> FromResidual<Option<Infallible>> for Option<T>
. Checking the libcore source, it appears that there's simply an impl<T> FromResidual for Option<T>
: The impl is "inheriting" the problematic way to spell the type parameter from the default.
Should we perhaps remove the default type parameter, if using it can lead to this problem? Even if it's just as a temporary measure while we fix whatever bug causes the issue: we can always add the default again later. In the meantime, would a PR changing the impl to not use the default parameter be accepted?
Here's a MRE, adapted from Zachs' multi-file MRE they shared on the Rust community discord: paste this in a library crate named example
, comment out either of the impls, and run cargo test --doc
to see how one compiles and one does not.
pub trait MyTry {
type Residual;
}
pub trait MyFromResidual<T> {}
pub struct MyOption<T>(T);
pub enum MyInfallible {}
impl<T> MyTry for MyOption<T> {
type Residual = Option<MyInfallible>;
}
// Of course these two impls won't both compile. Comment one out.
impl<T> MyFromResidual<MyOption<MyInfallible>> for MyOption<T> {}
impl<T> MyFromResidual<<Self as MyTry>::Residual> for MyOption<T> {}
/// using a doctest as a quick way to have an inline external crate
/// ```
/// use example::{MyFromResidual, MyOption};
/// struct Test;
/// impl<T> MyFromResidual<Test> for MyOption<T> {}
/// ```
pub struct Dummy;
For me personally, the main use I would like to see out of a stable Try v2 trait is the ability to write generic adapter types to modify what gets returned on a ?
, so that we no longer need to write try_xxx!()
macros (that match and return in a custom way) if we have control-flow-heavy code that wants to return standard types like Result
or Option
on Ok
/None
cases.
Such an API could look like this:
fn example(arg: Option<u8>) -> Option<u8> {
arg.try_some()?; // returns if arg is a `Some`
Some(42)
}
I tried to write up a complete example of this right now, but I'm running into the same issue as the previous poster about the FromResidual impl for Options. Still, here is my attempt: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=b1bf5652864308956812f72e71488430
For Results it works fine though, although I had to do a weird workaround to get a Infallible pattern match working, but that seems unrelated to the Try API: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=b826e7fd8c8f91e5186501f606f1bcb9
(Pattern match error for who is curios: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=56916aba1cee38c4b1376414cf5e379a)
So, in summary:
Option
implFromResidual
As for the Residual
trait, I have no specific opinion, nor have I looked into why or if we should have it. As far as I understand it, its an optional extra compononent to make writing generic code easier, so I have the following suggestion:
Lets split up this feature so that we can focus on stabilizing the Try+FromResidual
part first, and focus on Residual
in isolation.
Also, just an observation, but: When writing the code above, I started to wonder if using the Try
Self
type with an Infallible
type parameter is worth it, just to avoid defining an extra type. Because it makes the impls quite a bit harder to read, compared to just having a type like struct ResultResidue<T>(T)
. Eg this is what my custom impl looks like with such a type: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=02dff64bfe7b6d70d390aa6c0e73712c
This seems to be a important educational point to me, as explaining how the Output+Residue split works gets harder if you also need to wrap your head around the Infallible
trick. Eg explaining and understanding that a Result<T, E>
can be split into a T
and ErrResidue<E>
pair seems a bit simpler to me.
Here my project that use try trait, https://crates.io/crates/binator:
I didn't work since few months of this projet, I know I tell previously in this thread I will post it when I release it then it is haha not perfect but that something. That a pretty big project that use try trait in a very practical way.
try trait allow me to do really cool stuff having a type that better represent the result of a parser is very nice. and the user can use it like result or option with ?.
Experience report
I used try_traits_v2 to implement FromResidual for Result<Infallible, impl Into
After FromResidual was implemented I could change the return type on the visitors "iterator" and still use Err(EvalError)? to return errors inside an iterator, or return an empty iterator with None?. All the Ok-wrapping went away.
It was quite straightforward to figure out what was needed. The only stumbling blocks was that the default for R in FromResidual made the RustRover autocomplete the impl skeleton incorrectly for my usage, which caused a few minutes of head-scratching. The other thing that took a few tries was to see that the Ok type on the Result impl must be Infallible, but in that case the type errors from rustc were quite helpful.
All in all, great feature. Works for me. I don't mind the "Residual" name, even though it's not intuitive for me. Just another concept to learn. The default for R might cause more problems than it solves, though.
I recently tried this out by implementing an Either
type (isomorphic to Result
)
I still don't understand why the type of L
(equivalent to Ok
in Result
) can't be inferred. I needed to provide type arguments when I expected not too.
It's possible that I got something wrong or had incorrect expectations but wanted to share my experience in case this wasn't intended.
Thanks!
@paulyoung as you've written, L
is the equivalent of Err
. Try::Output
is the type of x?
. Second of all, you don't need all the types, just the ones that can't be inferred.
In let foo = Either::<String, _>::Right("foo".to_string())?;
and let foo_bar_baz = Either::Right(format!("{foo_bar} baz"))?
the left type can't be inferred because the compiler can't solve ?T
with the only bound being String: From<?T>
(there are many types for which this is true) (remember that L
is the Err
-like here). Note that you specify the From
bound in the FromResidual
impl (if you remove the F
and From
it will work):
impl<L, R, F: From<L>> FromResidual<Either<L, Infallible>> for Either<F, R> {
With let foo_bar = Either::<_, String>::Left(format!("{foo} bar"))?
the right is otherwise unbounded also (Left
is Err
-like, which means it is just returned and then the R
could be anything which implements Display
).
At risk of sounding overly ambitious, let's… ship this?
The overall sentiment has been positive. People feel it's solving real problems and filling an important gap.
Two quick decisions could unblock us:
FromResidual
: Keep the flexibility or simplify?Thoughts on pushing this across the finish line?
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