Skip to content
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 ? operator and try blocks (RFC 243, question_mark & try_blocks features) #31436

Open
10 of 11 tasks
nikomatsakis opened this issue Feb 5, 2016 · 353 comments
Open
10 of 11 tasks

Comments

@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Feb 5, 2016

Tracking issue for rust-lang/rfcs#243 and rust-lang/rfcs#1859.

Implementation concerns:

  • ? operator that is roughly equivalent to try! - #31954
  • try { ... } expression - #39849
    • resolve do catch { ... } syntax question
      • Resolved as try { .. }, - #50412
    • resolve whether catch blocks should "wrap" result value (first addressed in #41414, now being settled anew in #70941)
    • Address issues with type inference (try { expr? }? currently requires an explicit type annotation somewhere).
  • settle design of the Try trait (rust-lang/rfcs#1859)
    • implement new Try trait (in place of Carrier) and convert ? to use it (#42275)
    • add impls for Option and so forth, and a suitable family of tests (#42526)
    • improve error messages as described in the RFC (#35946)
  • reserve try in new edition
  • block try{}catch (or other following idents) to leave design space open for the future, and point people to how to do what they want with match instead
@reem
Copy link
Contributor

@reem reem commented Feb 5, 2016

The accompanying RFC discusses a desugaring based on labeled return/break, are we getting that too or will there just be special treatment for ? and catch in the compiler?

EDIT: I think labeled return/break is an excellent idea separate from ? and catch, so if the answer is no I will probably open a separate RFC for it.

@nikomatsakis
Copy link
Contributor Author

@nikomatsakis nikomatsakis commented Feb 5, 2016

Labeled return/break is purely for explanatory purposes.

On Fri, Feb 5, 2016 at 3:56 PM, Jonathan Reem notifications@github.com
wrote:

The accompanying RFC discusses a desugaring based on labeled return/break,
are we getting that too or will there just be special treatment for ? and
catch in the compiler?


Reply to this email directly or view it on GitHub
#31436 (comment).

@glaebhoerl
Copy link
Contributor

@glaebhoerl glaebhoerl commented Feb 5, 2016

Another unresolved question we have to resolve before stabilizing is what the contract which impls of Into have to obey should be -- or whether Into is even the right trait to use for the error-upcasting here. (Perhaps this should be another checklist item?)

@petrochenkov
Copy link
Contributor

@petrochenkov petrochenkov commented Feb 5, 2016

@reem

I think labeled return/break is an excellent idea ... I will probably open a separate RFC for it.

Please do!

@thepowersgang
Copy link
Contributor

@thepowersgang thepowersgang commented Feb 6, 2016

On the subject of the Carrier trait, here is a gist example of such a trait I wrote back early in the RFC process.
https://gist.github.com/thepowersgang/f0de63db1746114266d3

@petrochenkov
Copy link
Contributor

@petrochenkov petrochenkov commented Feb 6, 2016

How this is treated during parsing?

struct catch {
    a: u8
}

fn main() {

    let b = 10;
    catch { a: b } // struct literal or catch expression with type ascription inside?

}

@eddyb
Copy link
Member

@eddyb eddyb commented Feb 6, 2016

@petrochenkov Well, the definition couldn't affect parsing, but I think we still have a lookahead rule, based on the second token after {, : in this case, so it should still be parsed as a struct literal.

@petrochenkov
Copy link
Contributor

@petrochenkov petrochenkov commented Feb 6, 2016

Also

let c1 = catch { a: 10 };
let c2 = catch { ..c1 }; // <--

struct catch {}
let c3 = catch {}; // <--

+ rust-lang/rfcs#306 if (when!) implemented.
It seems like there are no conflicts besides struct literals.

Given the examples above I'm for the simplest solution (as usual) - always treat catch { in expression positions as start of a catch block. Nobody calls their structures catch anyway.

@durka
Copy link
Contributor

@durka durka commented Feb 6, 2016

It would be easier if a keyword was used instead of catch.

@bluss
Copy link
Member

@bluss bluss commented Feb 6, 2016

@durka
Copy link
Contributor

@durka durka commented Feb 6, 2016

@bluss yeah, I admit none of them are great... override seems like the only one that is close. Or we could use do, heh. Or a combination, though I don't see any great ones immediately. do catch?

@bluss
Copy link
Member

@bluss bluss commented Feb 6, 2016

do is the only one that seems to be close IMO. A keyword soup with do as prefix is a bit irregular, not similar to any other part of the language? Is while let a keyword soup as well? That one feels ok now, when you are used to it.

@est31
Copy link
Contributor

@est31 est31 commented Feb 7, 2016

port try! to use ?

Can't ? be ported to use try! instead? This would allow for the use case where you want to get a Result return path, e.g. when debugging. With try! this is fairly easy, you just override the macro at the beginning of the file (or in lib/main.rs):

macro_rules! try {
    ($expr:expr) => (match $expr {
        Result::Ok(val) => val,
        Result::Err(err) => {
            panic!("Error occured: {:?}", err)
        }
    })
}

You will get a panic stack trace starting from the first occurrence of try! in the Result return path. In fact, if you do try!(Err(sth)) if you discover an error instead of return Err(sth), you even get the full stack trace.

But when debugging foreign libraries written by people who haven't implemented that trick, one relies on try! usage somewhere higher in the chain. And now, if the library uses the ? operator with hardcoded behaviour, getting a stacktrace gets almost impossible.

It would be cool if overriding try! would affect the ? operator as well.

Later on when the macro system gets more features you can even panic! only for specific types.

If this proposal requires an RFC please let me know.

@rpjohnst
Copy link
Contributor

@rpjohnst rpjohnst commented Feb 7, 2016

Ideally ? could just be extended to provide stack trace support directly, rather than relying on the ability to override try!. Then it would work everywhere.

@durka
Copy link
Contributor

@durka durka commented Feb 7, 2016

Stack traces are just one example (though a very useful one, seems to me).
If the Carrier trait is made to work, maybe that can cover such extensions?

On Sun, Feb 7, 2016 at 4:14 PM, Russell Johnston notifications@github.com
wrote:

Ideally ? could just be extended to provide stack trace support directly,
rather than relying on the ability to override try!. Then it would work
everywhere.


Reply to this email directly or view it on GitHub
#31436 (comment).

@est31
Copy link
Contributor

@est31 est31 commented Feb 8, 2016

Without wanting to speculate, I think that it could work, albeit with some issues.

Consider the usual case where one has code returning some Result<V,E> value. Now we would need to allow for multiple implementations of the Carrier trait to coexist. In order to not run into E0119, one has to make all implementations out of scope (possibly through different traits which are per default not imported), and when using the ? operator, the user is required to import the wished implementation.

This would require everybody, even those who don't want to debug, to import their wished trait implementation when using ?, there would be no option for a predefined default.

Possibly E0117 can be an issue too if wanting to do custom Carrier implementations for Result<V,E>, where all types are outside bounds, so libstd should provide already provide a set of implementations of the Carrier trait with the most used use cases (trivial implementation, and panic! ing implementation, perhaps more).

Having the possibility to override via a macro would provide a greater flexibility without the additional burden on the original implementor (they don't have to import their wished implementation). But I also see that rust never had a macro based operator before, and that implementing ? via a macro isn't possible if catch { ... } is supposed to work, at least not without additional language items (return_to_catch, throw, labeled break with param as used in RFC 243).

I am okay with any setup which enables one to get Result stacktraces with an Err return path, while having only to modify a very small amount of code, prefferably at the top of the file. The solution should also work unrelated to how and where the Err type is implemented.

@rphmeier
Copy link
Contributor

@rphmeier rphmeier commented Feb 8, 2016

Just to chime in on bikeshedding: catch in { ... } flows pretty nicely.

@durka
Copy link
Contributor

@durka durka commented Feb 8, 2016

catch! { ... } is another backcompat choice.

@durka
Copy link
Contributor

@durka durka commented Feb 8, 2016

Also, not that I expect this to change anything, but a note that this is going to break multi-arm macros that were accepting $i:ident ?, in the same way that type ascription broke $i:ident : $t:ty.

@dgrunwald
Copy link
Contributor

@dgrunwald dgrunwald commented Feb 8, 2016

Don't overdo the backwards compatibility, just treat catch as a keyword when followed by { (possibly only in expression position, but I'm not sure if that changes much compatibility-wise).

I can also imagine some possible problems that don't involve struct literals (e.g. let catch = true; if catch {}); but I prefer a minor breaking change over a more ugly syntax.

Didn't we have a for adding new keywords, anyways? We could offer some kind of from __future__ opt-in for new syntax; or specify a rust language version number on the command-line / in Cargo.toml.
I highly doubt that in the long term, we'll be able to work with only those keywords that are already reserved. We don't want our keywords to have three different meanings each, depending on context.

@glaebhoerl
Copy link
Contributor

@glaebhoerl glaebhoerl commented Feb 9, 2016

I agree. This isn't even the first RFC where this has come up (rust-lang/rfcs#1444 is another example). I expect it won't be the last. (Also default from rust-lang/rfcs#1210, although it's not an RFC I'm in favor of.) I think we need to find a way to add honest-to-god keywords instead of trying to figure out how to ad-hoc hack the grammar for every new case.

@rkjnsn
Copy link
Contributor

@rkjnsn rkjnsn commented Feb 26, 2016

Wasn't the whole argument for not reserving several keywords prior to 1.0 that we'd definitely be introducing a way to add new keywords to the language backward compatibly (possibly by explicitly opting in), so there was no point? Seems like now would be a good time.

@aturon
Copy link
Member

@aturon aturon commented Feb 26, 2016

@japaric Are you interested in reviving your old PR and taking this on?

@japaric
Copy link
Member

@japaric japaric commented Feb 26, 2016

@aturon My implementation simply desugared foo? in the same way as try!(foo). It also only worked on method and function calls, i.e. foo.bar()? and baz()? work but quux? and (quux)? don't. Would that be okay for an initial implementation?

@eddyb
Copy link
Member

@eddyb eddyb commented Feb 26, 2016

@japaric What was the reason for restricting it to methods and function calls? Wouldn't parsing it be easier as a general postfix operator?

@ijackson
Copy link
Contributor

@ijackson ijackson commented Aug 2, 2020

I'm keen to see this stabilised. Having read this thread, and #70941, I think the summary should be updated as follows:

  1. "resolve whether catch blocks should "wrap" result value" should be ticked, "Resolved as yes"

  2. New concern added about these inference problems. Perhaps something like:

    • Ergonomic difficulties due to problems with type inference.

ISTM that this last concern could be addressed by, amongst other ways:

  • Add a bespoke type ascryption syntax to try, before stabilisation
  • Decide that rust-lang/rfcs#803 #23416 (Type ascription) will address this and should be stabilised first
  • Add some kind of automatic fallback (eg to Result, perhaps as suggested in #31436 (comment)
  • Decide to stabilise try as-is, now, leaving syntactic/semantic space for future improvement

(Some of these options are not mutually exclusive.)

Thanks for your attention and I hope you find this message helpful.

@joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented Aug 2, 2020

Type ascription has a number of issues (syntactic and otherwise), and seems unlikely to get implemented soon, let alone stabilized; blocking try blocks on type ascription syntax doesn't seem appropriate.

A fallback to Result might help, but doesn't solve the type inference problems with error types: try { expr? }? (or in practice more complex equivalents) have effectively two calls to .into(), which gives the compiler too much flexibility on the intermediate type.

@nikomatsakis
Copy link
Contributor Author

@nikomatsakis nikomatsakis commented Aug 5, 2020

@ijackson thanks for taking the initiative to summarize the current state. I think you're correct that there are various ways we could improve on try blocks, but one of the problems is that we're not sure which one to do, in part because each solution has its own unique drawbacks.

With regard to type ascription, though, I do feel like the implementation challenges there aren't that difficult. That might be a good candidate to put some attention into and try to push it over the finish line regardless. I don't recall whether there was much controversy about the syntax or anything like that.

@joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented Aug 7, 2020

@ijackson
Copy link
Contributor

@ijackson ijackson commented Nov 2, 2020

Personally I think the ergonomic problems are not so bad that it is not worth stabilising this feature now. Even without expression type ascription, introducing a let binding is not so ugly a workaround.

Also, try blocks might be useful in macros. In particular, I wonder if @withoutboats excellent fehler library would suffer from fewer problems with deficiencies in our macro system, if it could wrap the bodies of procs in try.

@steveklabnik
Copy link
Member

@steveklabnik steveklabnik commented Nov 6, 2020

I run into places where I would love to use try blocks a lot. It would be good to get this over the line. Personally, I would absolutely, 100% sacrifice type ascription if it were required to get try blocks over the line. I have yet to find myself in a situation where I said "dang I would love to have type ascription here," but end up doing an IIFE to simulate try blocks a lot. Leaving a long-term, useful feature unstable because it conflicts with another long-term, unstable feature is a really unfortunate situation.

To be slightly more specific, I find myself doing this when I am inside a function that returns Result, but I want to do some sort of processing on things that return an Option. That said, if Try in general were stable, I would still probably prefer try blocks, as I don't actually want to return from the main function to do so, but instead, give some sort of default value if anything in the chain is None. This tends to happen for me in serialization style code.

@mark-i-m
Copy link
Member

@mark-i-m mark-i-m commented Nov 6, 2020

Personally, I've wanted type ascription far more often than try blocks (though i have wanted both at times). In particular, I've often struggled with "type debugging" where the compiler infers a different type from what i expected. Usually, you have to add a new let binding somewhere, which is really disruptive and causes rustfmt to break the undo history. Moreover, there are lots of places where type ascription would avoid an extra turbo fish.

In constrast, I can just use and_then or other combinators to terminate early without exiting. Perhaps not as clean aa try blocks, but not that bad either.

@withoutboats
Copy link
Contributor

@withoutboats withoutboats commented Nov 6, 2020

@steveklabnik @mark-i-m Try blocks and type ascription are not in any way in conflict, it is not a question of one feature or another. It's just try blocks have unergonomic type inference failures, and generalized type ascription could be a way to solve that problem, but since generalized type ascription is not a near term feature or even a sure thing, @joshtriplett (and I agree) doesn't want this feature to block on generalized type ascription happening.

This doesn't even mean we wouldn't make generalized type ascription the solution to the problem; one option deserving investigation is "stabilize try as is, expecting that someday generalized type ascription will solve that problem." All that's been said is don't block stabilization on type ascription.


@rust-lang/lang I have to admit its a bit hard to understand the nuance of the type inference failure from this thread, because of the limitations of GitHub and the many other subjects that are discussed here. Since reaching a decision about how to handle the inference failures is the only thing blocking try blocks from stabilizing, I think it would be beneficial if we had a meeting to discuss this, and someone could take point on getting a deep understanding of the inference issue.

One question that occurs to me, for example: is this inference issue specifically because of the conversion flexibility we've allowed in Try? I know that decision has been discussed to death, but if that's so this seems like pertinent new information that could justify changing the definition of the Try trait.

@nikomatsakis
Copy link
Contributor Author

@nikomatsakis nikomatsakis commented Nov 6, 2020

@withoutboats I agree with the need to collect all the information in one place and the desire to push this feature over the finish line. That said, I think the last time we investigated here it also became clear that changes to Try might be difficult because of backwards compatibility -- @cramertj mentioned some specific Pin impls, IIRC.

@jnicholls
Copy link

@jnicholls jnicholls commented Dec 4, 2020

Like @steveklabnik, I am constantly doing IIFE to simulate try blocks. Thank you all for considering the prioritization of this feature.

@cramertj
Copy link
Member

@cramertj cramertj commented Dec 8, 2020

@nikomatsakis small nit: the Poll implementations are the interesting ones (not Pin).

@NilSet
Copy link
Contributor

@NilSet NilSet commented Jan 3, 2021

Is there anything remaining which suggests this should not be stabilized as is? It seems like inference fallbacks or more general type ascription are both solutions which will improve the ergonomics at a later date, and will not be hindered by stabilizing try blocks as is.

@scottmcm
Copy link
Member

@scottmcm scottmcm commented Jan 10, 2021

but if that's so this seems like pertinent new information that could justify changing the definition of the Try trait.

Agreed, boats! I've posted an RFC with a potential new Try design: rust-lang/rfcs#3058

For scoping reasons it doesn't try to specify exactly how try should work, leaving that to a future RFC, but it discusses the problem and some variations in the future possibilities section: https://github.com/scottmcm/rfcs/blob/do-or-do-not/text/0000-try-trait-v2.md#possibilities-for-try (I don't know if it's sufficiently clear about the inference trouble with the "normal" ? desugar. Let me know on that thread or zulip or wherever if there's something you'd like me to expand.)

@ciphergoth
Copy link
Contributor

@ciphergoth ciphergoth commented Jan 11, 2021

Agreed, boats! I've posted an RFC with a potential new Try design: rust-lang/rfcs#3058

I can't figure out - does this block stabilization of "try" as is? In other words, if it were stabilized as is, would that hinder a later move to the design in rust-lang/rfcs#3058?

@scottmcm
Copy link
Member

@scottmcm scottmcm commented Jan 11, 2021

@ciphergoth try { E } as currently implemented on nightly is the "always requires context" version. So the question is whether we want that syntax to be that version. If we do, then we could stabilize it without waiting for 3058.

The problem is that it's not obvious to me that that syntax should be the one that uses context. Perhaps it would make sense for try { E } to be the one that doesn't use context (and thus doesn't do error-conversion), and for try 🤷 T { E } to be the one that produces a T (and uses that as its context). If that's what we want, then we shouldn't stabilize try { E } right now.

Said otherwise, see comments such as #70941 (comment) for annotation concerns. Someone needs to figure those out before this is ready to be stabilized -- and the resolution there will decide which parts (if any) could be stabilized without 3058.

@alexschrod
Copy link

@alexschrod alexschrod commented Nov 8, 2021

I don't know if this is a helpful example or just noise at this point, but I recently came across a case where the try block couldn't infer the type despite an explicit turbofish, and where the same code converted to an IIFE (which is the method I use on stable to get something akin to "try block behavior") works just fine. Top one gives compiler error, bottom one works:

fn using_try(file: File) {
    BufReader::new(file)
        .lines()
        .find_map(|line| try { Ok::<_, std::io::Error>(parse_line(line?)) }.transpose());
}

fn using_iife(file: File) {
    BufReader::new(file)
        .lines()
        .find_map(|line| (|| Ok::<_, std::io::Error>(parse_line(line?)))().transpose());
}
error[E0282]: type annotations needed
  --> src/lib.rs:10:26
   |
10 |         .find_map(|line| try { Ok::<_, std::io::Error>(parse_line(line?)) }.transpose());
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type
   |
   = note: type must be known at this point

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

I really don't know what more type annotation it wants, what I'm returning is unequivocally Result<(), std::io::Error>.

@scottmcm
Copy link
Member

@scottmcm scottmcm commented Nov 9, 2021

Reminder that try{} includes success-wrapping (#70941), so you probably don't want the Ok in using_try.

One way that works is

fn using_try(file: File) {
    BufReader::new(file)
        .lines()
        .find_map(|line| Result::<_, std::io::Error>::transpose(try { parse_line(line?) }));
}

Because inherent method resolution requires that the type is already decided -- when there's a non-trait method call involved, it blocks inference.

Something like https://rust-lang.github.io/rfcs/3058-try-trait-v2.html#possibilities-for-try would help, since try would no longer need contextual information, and would probably make
try { parse_line(line?) }.transpose() start to work without anything else.

That said, part of the problem is that the method isn't using the result of find_map. For example, this works:

fn using_try(file: File) -> Option<Result<(), std::io::Error>> {
    BufReader::new(file)
        .lines()
        .find_map(|line| Result::transpose(try { parse_line(line?) }))
}

(And of course, so does .find_map(|line| line.map(parse_line).transpose()), but that's not important for the try block thread.)

@K4rakara
Copy link

@K4rakara K4rakara commented Nov 25, 2021

Regarding the issues with type inference, could something like this be done?

use anyhow::Result;

match try::<Result<()>> {
    some_result_fn()?
} {
  Ok(()) => (),
  Err(error) => panic!("Error occurred: {:?}", error),
}

certainly beats the current way:

use anyhow::Result;

let result: Result<()> = try {
    some_result_fn()?
};

match result {
  Ok(()) => (),
  Err(error) => panic!("Error occurred: {:?}", error),
}

If proposing this would require an RFC, just let me know, I haven't contributed to the language that much.

@piegamesde
Copy link
Contributor

@piegamesde piegamesde commented Nov 25, 2021

Wasn't there a proposal for explicit type hints that ought to solve this, that was initially designed for .into() type annotations? IIRC it would allow something like try {…} as Result<()>

@ijackson
Copy link
Contributor

@ijackson ijackson commented Nov 25, 2021

Wasn't there a proposal for explicit type hints that ought to solve this, that was initially designed for .into() type annotations? IIRC it would allow something like try {…} as Result<()>

I think you may mean type ascription, RFC803, #23416

@scottmcm
Copy link
Member

@scottmcm scottmcm commented Dec 8, 2021

Update: With the new (unstable) ops::Residual trait (#91285) added in #91286, there's a way forward for a version of try{} that would work like array::try_map and Iterator::try_find and friends now do (#85115). I hope to write an RFC to that effect in the coming weeks.

@dbsxdbsx
Copy link

@dbsxdbsx dbsxdbsx commented Jan 16, 2022

I just got to this issue when I am trying to use ? with try inside a closure, suggested here: https://stackoverflow.com/questions/62687455/alternatives-for-using-the-question-mark-operator-inside-a-map-function-closure
, Hope this feature could be stable soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet