rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
96.71k stars 12.5k forks source link

Tracking Issue for `try_trait_v2`, A new design for the `?` desugaring (RFC#3058) #84277

Open scottmcm opened 3 years ago

scottmcm commented 3 years ago

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

Unresolved Questions

From RFC:

From experience in nightly:

Implementation history

rdrpenguin04 commented 1 year ago

What still needs to be done?

onestacked commented 1 year ago

From what I understand the main blocking points are:

rdrpenguin04 commented 1 year ago

Alright, response:

Thank you for the answer; I look forward to pushing this through :smile:

rdrpenguin04 commented 11 months ago

@scottmcm Would you be willing to update your checklist to reflect newer changes?

T-Dark0 commented 10 months ago

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;
Kimundi commented 8 months ago

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:


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.

Stargateur commented 6 months ago

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

luksan commented 4 months ago

Experience report

I used try_traits_v2 to implement FromResidual for Result<Infallible, impl Into> and Option for a custom Iterator<Item=Result<Value,EvalError>>. I use this in an AST walker where each node can return zero or more Values, or an error. Before I implemented FromResidual I had to use Result<iterator, EvalError> as return type in the visitor methods in order to use "?", which caused a lot of Ok-wrapping and having to handle the fact that an error could be both the outer result or inside the iterator. Also, returning an empty iterator was very explicit.

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.

paulyoung commented 3 months ago

I recently tried this out by implementing an Either type (isomorphic to Result)

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=4301a1db0befb2c3b68b5a94abb28c32

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!

WaffleLapkin commented 3 months ago

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

ianks commented 1 month ago

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:

  1. FromResidual: Keep the flexibility or simplify?
  2. Stick with Output/Residual or change? If someone is passionate about this one, please speak up but let’s not shed too much

Thoughts on pushing this across the finish line?