-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tracking Issue for try_trait_v2
, A new design for the ?
desugaring (RFC#3058)
#84277
Comments
Implement the new desugaring from `try_trait_v2` ~~Currently blocked on rust-lang#84782, which has a PR in rust-lang#84811 Rebased atop that fix. `try_trait_v2` tracking issue: rust-lang#84277 Unfortunately this is already touching a ton of things, so if you have suggestions for good ways to split it up, I'd be happy to hear them. (The combination between the use in the library, the compiler changes, the corresponding diagnostic differences, even MIR tests mean that I don't really have a great plan for it other than trying to have decently-readable commits. r? `@ghost` ~~(This probably shouldn't go in during the last week before the fork anyway.)~~ Fork happened.
Implement the new desugaring from `try_trait_v2` ~~Currently blocked on rust-lang/rust#84782, which has a PR in rust-lang/rust#84811 Rebased atop that fix. `try_trait_v2` tracking issue: rust-lang/rust#84277 Unfortunately this is already touching a ton of things, so if you have suggestions for good ways to split it up, I'd be happy to hear them. (The combination between the use in the library, the compiler changes, the corresponding diagnostic differences, even MIR tests mean that I don't really have a great plan for it other than trying to have decently-readable commits. r? `@ghost` ~~(This probably shouldn't go in during the last week before the fork anyway.)~~ Fork happened.
We have a problem in our project related to the new question mark desugaring. We use the After updating to the latest nightly toolchain this stack trace collection started to work differently. I've created a small project for the demo: https://github.com/artemii235/questionmark_track_caller_try_trait_v2
Is there a way to make the track caller work the same way as it was before? Maybe we can use some workaround in our code? Thanks in advance for any help! |
That's interesting -- maybe |
From the description:
@artemii235 Do you mind opening a separate issue? |
No objections at all 🙂 I've just created it #87401. |
May i suggest changing |
How do I use |
Use |
Why the implementation of
Clarification is welcome as an error type implementing only |
see #31436 (comment) |
Hi, I'm keen to see this stabilized. Is there any work that can be contributed to push this forward? It would be my first Rust contribution, but I have a little experience working on compiler code (little bit of LLVM and KLEE in college). |
@BGR360 Unfortunately the main blockers here are unknowns, not concrete-work-needing-to-be-done, so it's difficult to push forward. It's hard to ever confirm for sure that people don't need the trait split into parts, for example. Have you perhaps been trying it out on nightly? It's be great to get experience reports -- good or bad -- about how things went. (For example, #42327 (comment) was a big help in moving to this design from the previous one.) If it was good, how did you use it? If it was bad, what went wrong? In either case, was there anything it kept you from doing which you would have liked to, even if you didn't need it? |
Experience Report@scottmcm I have tried Overall my experience with this feature is positive. It may end up being critical for my professional work in Rust. My use case is very similar to @artemii235: #84277 (comment). At my work, we need a way to capture the sequence of code locations that an error propagates through after it is created. We aren't able to simply use We would love to be able to do this in our Rust code using just the To get this to work, I made express use of the fact that you can implement multiple different I'd be happy to give more specifics on how I achieved my use case either here or on Zulip, just let me know :) Pros:
Cons:
|
Experience reportI was using Pros:
Cons:
Overall this v2 is a clear downgrade for this particular use case however the end result isn't too bad. If this is making other use cases possible it is likely worth it with better names and docs. The full change: https://gitlab.com/kevincox/ecl/-/commit/a1f348633afd2c8dd269f95820f95f008b461c9e |
This is actually a little bit unfortunate, in retrospect. It would be much better if I could just make use of
To illustrate, here's how things work in my experiment: pub struct ErrorStack<E> {
stack: ..,
inner: E,
}
impl<E> ErrorStack<E> {
/// Construst new ErrorStack with the caller location on top.
#[track_caller]
fn new(e: E) -> Self { ... }
/// Push location of caller to self.stack
#[track_caller]
fn push_caller(&mut self) { ... }
/// Return a new ErrorStack with the wrapped error converted to F
fn convert_inner<F: From<E>>(f: F) -> ErrorStack<F> { ... }
}
pub enum MyResult<T, E> {
Ok(T),
Err(ErrorStack<E>),
}
pub use MyResult::Ok;
pub use MyResult::Err;
impl<T, E> Try for MyResult<T, E> {
type Output = T;
type Residual = MyResult<Infallible, E>;
/* equivalent to std::result::Result's Try impl */
}
/// Pushes an entry to the stack when one [`MyResult`] is coerced to another using the `?` operator.
impl<T, E, F: From<E>> FromResidual<MyResult<Infallible, E>> for MyResult<T, F> {
#[inline]
#[track_caller]
fn from_residual(residual: MyResult<Infallible, E>) -> Self {
match residual {
// seems like this match arm shouldn't be needed, but idk the compiler complained
Ok(_) => unreachable!(),
Err(mut e) => {
e.push_caller();
Err(e.convert_inner())
}
}
}
}
/// Starts a new stack when a [`std::result::Result`] is coerced to a [`Result`] using `?`.
impl<T, E> FromResidual<std::result::Result<Infallible, E>> for Result<T, E> {
#[inline]
#[track_caller]
fn from_residual(residual: std::result::Result<Infallible, E>) -> Self {
match residual {
// seems like this match arm shouldn't be needed, but idk the compiler complained
std::result::Result::Ok(_) => unreachable!(),
std::result::Result::Err(e) => Err(StackError::new(e)),
}
}
} If impl<E, F: From<E>> From<ErrorStack<E>> for ErrorStack<F> {
#[track_caller]
fn from(mut e: ErrorStack<E>) -> Self {
e.push_caller();
e.convert_inner()
}
} However, this does not work because it conflicts with the blanket I could limit my fn foo() -> MyResult<(), io::Error> {
fs::File::open("foo.txt")?;
}
fn bar() -> MyResult<(), io::Error> {
// I need bar to show up in error traces, so I wrap with Ok(..?).
// Without my custom MyResult, I am unable to intercept this invocation of the `?` operator, because
// the return type is the same as that of `foo`.
Ok(foo()?)
} |
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: rust-lang/rfcs#3058 (comment)
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 I think that I feel like maybe more effort should be put into ensuring that the |
@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
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.). e.g. (rust-like pseudo-code)enum ControlFlow<B, C> {
/// Exit the operation without running subsequent phases.
Break(B),
/// Move on to the next phase of the operation as normal.
Continue(C),
}
monad_morph<B, C> ControlFlow<B, C> {
type Monadic<C> = ControlFlow<B, C>;
fn pure<C>(t: C) -> Self::Monadic<C> {
ControlFlow::Continue(t)
}
fn flat_map<C1, C2, F>(input: Self::Monadic<C1>, f: F) -> Self::Monadic<C2>
where F: /* function trait used might vary per monad_morph */ FnOnce(C1) -> Self::Monadic<C2>,
{
match input {
ControlFlow::Break(b) => ControlFlow::Break(b),
ControlFlow::Continue(c) => f(c),
}
}
/* a question that remains here would be how to handle the "short-circuiting" generally and effectively (that is, without many nested closures)
* probably similar to the residual stuff, but it would be interesting to see how it could be done generally
* (e.g. simple ControlFlow), while also allowing lazier interfaces (like monads similar to iterators (considering `flat_map` there)) */
} also, another idea would be to introduce some kind of "tagged |
That is already kinda the case. If you're using a 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 For what it's worth, I think things like 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 |
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? |
What still needs to be done? |
From what I understand the main blocking points are:
|
Alright, response:
Thank you for the answer; I look forward to pushing this through 😄 |
@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 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 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 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 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 Lets split up this feature so that we can focus on stabilizing the Also, just an observation, but: When writing the code above, I started to wonder if using the 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 |
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> 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. |
I recently tried this out by implementing an I still don't understand why the type of 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, In impl<L, R, F: From<L>> FromResidual<Either<L, Infallible>> for Either<F, R> { With |
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:
Thoughts on pushing this across the finish line? |
…=scottmcm Explicitly specify type parameter on FromResidual for Option and ControlFlow. ~~Remove type parameter default `R = <Self as Try>::Residual` from `FromResidual`~~ _Specify default type parameter on `FromResidual` impls in the stdlib_ to work around rust-lang#99940 / rust-lang#87350 ~~as mentioned in rust-lang#84277 (comment). This does not completely fix the issue, but works around it for `Option` and `ControlFlow` specifically (`Result` does not have the issue since it already did not use the default parameter of `FromResidual`). ~~(Does this need an ACP or similar?)~~ ~~This probably needs at least an FCP since it changes the API described in [the RFC](rust-lang/rfcs#3058). Not sure if T-lang, T-libs-api, T-libs, or some combination (The tracking issue is tagged T-lang, T-libs-api).~~ This probably doesn't need T-lang input, since it is not changing the API of `FromResidual` from the RFC? Maybe needs T-libs-api FCP?
…=scottmcm Explicitly specify type parameter on FromResidual for Option and ControlFlow. ~~Remove type parameter default `R = <Self as Try>::Residual` from `FromResidual`~~ _Specify default type parameter on `FromResidual` impls in the stdlib_ to work around rust-lang#99940 / rust-lang#87350 ~~as mentioned in rust-lang#84277 (comment). This does not completely fix the issue, but works around it for `Option` and `ControlFlow` specifically (`Result` does not have the issue since it already did not use the default parameter of `FromResidual`). ~~(Does this need an ACP or similar?)~~ ~~This probably needs at least an FCP since it changes the API described in [the RFC](rust-lang/rfcs#3058). Not sure if T-lang, T-libs-api, T-libs, or some combination (The tracking issue is tagged T-lang, T-libs-api).~~ This probably doesn't need T-lang input, since it is not changing the API of `FromResidual` from the RFC? Maybe needs T-libs-api FCP?
Rollup merge of rust-lang#128954 - zachs18:fromresidual-no-default, r=scottmcm Explicitly specify type parameter on FromResidual for Option and ControlFlow. ~~Remove type parameter default `R = <Self as Try>::Residual` from `FromResidual`~~ _Specify default type parameter on `FromResidual` impls in the stdlib_ to work around rust-lang#99940 / rust-lang#87350 ~~as mentioned in rust-lang#84277 (comment). This does not completely fix the issue, but works around it for `Option` and `ControlFlow` specifically (`Result` does not have the issue since it already did not use the default parameter of `FromResidual`). ~~(Does this need an ACP or similar?)~~ ~~This probably needs at least an FCP since it changes the API described in [the RFC](rust-lang/rfcs#3058). Not sure if T-lang, T-libs-api, T-libs, or some combination (The tracking issue is tagged T-lang, T-libs-api).~~ This probably doesn't need T-lang input, since it is not changing the API of `FromResidual` from the RFC? Maybe needs T-libs-api FCP?
@scottmcm This was discussed a few weeks ago in a libs-api meeting in which you participated. We decided that this feature probably needs a dedicate design meeting. Could you prepare for the design meeting a document breaking down the open questions, what could be partially stabilized, etc. We would then discuss this during in the first hour of the next libs-api meeting (or the next one after you have had a chance to compile the document). |
Is there a bad interaction between FromResidual and the orphan rule? The following violates the orphan rule:
I have a function which wants to return partial output if it's forced to stop partway through. In this particular case, it's a data structure search function which wants to record the major steps of a failing search to inform a potential future insert. I want to write I'm successfully using these same traits to implement Option -> () short circuiting, so I could use a I haven't read the discussions about these traits carefully, and am too much of a beginner to have a concrete suggestion, a confident analysis, or even confidence that what I want is reasonable and the answer isn't "better try block ergonomics". But it seems suspicious to me that I'm hitting orphan rule issues given that I'm using a local type to define an Option -> E conversion. |
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 rust-lang/rfcs#1859, tracked in #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 updateRemove theTryV2
alias #88223?
desugaring #85133FromResidual
but notTry
FromResidual
better (Issue rustdoc removes Try from <Self as Try>::Residual in std::ops::FromResidual #85454)Infallible
are either fine that way or have been replaced by!
Iterator::try_fold
try_fold
for iterators for internal iteration #62606)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 (Fix the types in one of theFromResidual
implementations rwf2/Rocket#1645). But that would add more friction for cases not using theFoo<!>
pattern, so may not be worth it.array::{try_from_fn, try_map}
andIterator::try_find
generic overTry
#91286, that might look like changing the associatedtype Residual;
totype Residual: Residual<Self::Output>;
.Implementation history
try_trait_v2
library basics #84092try_trait
fromstdarch
, Remove#![feature(try_trait)]
from a test stdarch#1142try_trait_v2
#84767The text was updated successfully, but these errors were encountered: