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

RFC: Reserve try for try { .. } block expressions #2388

Merged
merged 11 commits into from
May 3, 2018

Conversation

Centril
Copy link
Contributor

@Centril Centril commented Apr 4, 2018

πŸ–ΌοΈ Rendered

⏭ Tracking issue

πŸ“ Summary

This RFC reserves try as a keyword in edition 2018 for expressions of the form try { .. }.

πŸ’– Thanks

To @scottmcm, @Manishearth, for discussions and to everyone who participated in the internals thread

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label Apr 4, 2018
- **Used as crate?** *No*, as above.
- **Usage (sourcegraph):** **0** regex: N/A
7. **Consistency with old learning material:** Untaught

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're missing an alternative here -- a completely new keyword. Could be result. That has a higher chance of breakage but it's edition'd so maybe not an issue? I prefer catch over try but I strongly prefer something new over both.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see trap, fallible, collect, capture, etc all being considered later in the RFC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now added a review of the result keyword below trap.

@Ixrec
Copy link
Contributor

Ixrec commented Apr 4, 2018

In the current RFC text I see a dozen different keywords being evaluated against the same set of criteria, but there's just so much text that it would take a lot of careful rereads to get an overall feel for how that set of criteria judges all the various options, much less decide if the criteria are persuasive or not.

Could we try organizing most of the comparative information in a table so that it's easier to see how all the options compare?

@Centril
Copy link
Contributor Author

Centril commented Apr 4, 2018

@Ixrec

[..] but there's just so much text that it would take a lot of careful rereads to get an overall feel for how that set of criteria judges all the various options, [..]

I hear you ;) There's certainly a lot of text!

[..] much less decide if the criteria are persuasive or not.

These are lifted mostly from the ? and catch { .. } RFC, see: https://github.com/rust-lang/rfcs/blob/master/text/0243-trait-based-exception-handling.md#choice-of-keywords

Could we try organizing most of the comparative information in a table so that it's easier to see how all the options compare?

Hmm... That would be helpful, but it's not very easy to do this in markdown; at least for me -- and it's quite a lot of busy work. A PR adding a table would be welcome though. I'll see if I have time to summarize it myself otherwise.

@clarfonthey
Copy link
Contributor

clarfonthey commented Apr 4, 2018

I still like catch a lot better because it specifically annotates that the error will be "caught" on the catch { ... } boundary, whereas try { ... } doesn't really clarify that.

Also, nested catch makes sense to me because it has nested layers of "catching" errors, whereas nested "trying" doesn't really feel that way.

I'd also like to point out that having catch by itself will make people immediately notice the difference from languages like C++ and Java, whereas having try by itself will mean that a lot of people will try adding catch and notice that they can't actually do that.

Also… I'd like to point out that the trait being Try doesn't super matter at this point because it's not stable yet. We could easily change it to Catch and all would be fine.

@clarfonthey
Copy link
Contributor

clarfonthey commented Apr 4, 2018

Also I think that the catch might also work if we allow a type to be "catched," like:

let first_two = catch Option<_> {
    let (first, rest) = self.split_first()?;
    let (second, rest) = rest.split_first()?;
    (first, second, rest)
};

which would by sugar for type ascription (which, would end up ascribing after the }, which sucks).

If we did this with try, it feels more like "try to make an Option, but if that doesn't work ..." when the Option is the only type of thing we're "trying."

This also lends very, very nicely to an Ok-wrapping function syntax of fn f(...) -> catch Result<T, E> { ... }, which is simply sugar for fn f(...) -> Result<T, E> { catch { ... } }.

@Centril
Copy link
Contributor Author

Centril commented Apr 4, 2018

I still like catch a lot better because it specifically annotates that the error will be "caught" on the catch { ... } boundary, whereas try { ... } doesn't really clarify that.

My view is that this might work in isolation, but not if we consider the established meaning of catch in other languages as a handler. But Rust does not exist in isolation. You can always say that "try { expr } tries the expr and if it fails, errors will not escape the block."

I'd also like to point out that having catch by itself will make people immediately notice the difference from languages like C++ and Java, whereas having try by itself will mean that a lot of people will try adding catch and notice that they can't actually do that.

Allowing catch handlers after try would aid in ergonomics tho; and if we go with try then that option is open to us in a legible way after with: try { expr } catch { err => manipulateErr(err) }. It's also possible to do specific diagnostics tailored to try { expr } catch ... if we don't want to support that.

Also… I'd like to point out that the trait being Try doesn't super matter at this point because it's not stable yet. We could easily change it to Catch and all would be fine.

We can change the name of the trait, but we should not imo. People already call ? the "try-operator" and so having try { .. } aids in learning.

Also I think that the catch might also work if we allow a type to be "catched," like:

We can equally write:

let first_two = try : Option<_> {
    let (first, rest) = self.split_first()?;
    let (second, rest) = rest.split_first()?;
    (first, second, rest)
};

The colon makes it much more clear that this is type ascription at play, but in this case, writing:

let first_two: Option<_> = try {
    let (first, rest) = self.split_first()?;
    let (second, rest) = rest.split_first()?;
    (first, second, rest)
};

seems the simpler solution.

This also lends very, very nicely to an Ok-wrapping function syntax of fn f(...) -> catch Result<T, E> { ... }, which is simply sugar for fn f(...) -> Result<T, E> { catch { ... } }.

Equally possible as one of:

try fn f(...) -> Result<T, E> { ... }
fn f(...) -> try Result<T, E> { ... }
fn f(...) -> Result<T, E> = try { ... }

Here, my personal favorite is the last version (= try {..}) cause it disturbs flow least, and generalizes well to async, etc.

@Manishearth
Copy link
Member

Manishearth commented Apr 4, 2018

We can change the name of the trait, but we should not imo. People already call ? the "try-operator" and so having try { .. } aids in learning.

IMO this is an argument against try {} since ?/Try means the inverse operation of the proposed try {}

@nikomatsakis
Copy link
Contributor

I'm in favor of try as the keyword for a few reasons:

  • I think it is good for us to leverage people's intuitions when we can, and I think try matches those intuitions reasonably well.
  • It leaves room for expanding to try { } catch { } in the future, if we wanted to integrate a match on the resulting try.
    • But we don't have to.
  • I'm not too concerned about try! -- code that uses it already looks quite dated to me, and I think that people will be learning ? first.

To expand on the first point, let me copy and paste some text from my post on the internals thread.

I think that there is a belief -- one that I have shared from time to time -- that it is not helpful to use familiar keywords unless the semantics are a perfect match, the concern being that they will setup an intuition that will lead people astray. I think that is a danger, but it works both ways: those intuitions also help people to understand, particularly in the early days. So it's a question of "how far along will you get before the differences start to matter" and "how damaging is it if you misunderstand for a while".

I'll share an anecdote. I was talking at a conference to Joe Armstrong and I asked him for his take on Elixir. He was very positive, and said that he was a bit surprised, because he would have expected that using more familiar, Ruby-like syntax would be confusing, since the semantics of Erlang can at times be quite different. But that it seemed that the opposite was true: people preferred the familiar syntax, even when the semantics had changed. (I am paraphrasing, obviously, and any deviance from Joe's beliefs are all my fault.)

I found that insight pretty deep, actually. It's something that I've had kicking around in my brain -- and I know people in this community and elsewhere have told me before -- but somehow it clicked that particular time.

Rust has a lot of concepts to learn. If we are going to succeed, it's essential that people can learn them a bit at a time, and that we not throw everything at you at once. I think we should always be on the lookout for places where we can build on intuitions from other languages; it doesn't have to be a 100% match to be useful.

@Centril
Copy link
Contributor Author

Centril commented Apr 4, 2018

@Manishearth

I think Try should be considered the "carrier" of the exception-effect, and so Try should not really be considered as the "Try operator trait", but rather the "Exception carrier trait" which is implemented by types that have success and failure modes.

Being able to search for try and get both documentation about ? and try { .. } is also beneficial to learnability imo since two tightly linked concepts can be easily found from each other.
Being inverses in the case of try { x?; } == x seems fine to me; we can distinguish between them by calling ? the try operator and try { .. } the try scoping block.

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Apr 4, 2018

@clarcharr

Also I think that the catch might also work if we allow a type to be "catched," like:

let first_two = catch Option<_> {
    let (first, rest) = self.split_first()?;
    let (second, rest) = rest.split_first()?;
    (first, second, rest)
};

This is actually a good point. I think we probably do want the ability to specify the type that will be caught -- in fact, I almost would go so far as to say we want to make it mandatory to specify that type. The problem is that if you don't specify it, and it winds up not being constrained from somewhere else, then you get errors because we don't know what to coerce ? into. In general, knowing the type will be helpful also with "Ok-wrapping".

I suppose try as Result<> { ... } is also ok?

In practice I usually write this today:

let x: Ty = do catch { .. };

While I'm generally against reserving more than we need, maybe we should reserve both try/catch to be safe :)

@Centril
Copy link
Contributor Author

Centril commented Apr 4, 2018

@nikomatsakis

I almost would go so far as to say we want to make it mandatory to specify that type.

Let's keep it at almost ;) forcing people to always write out the type will make try { .. } kinda unpopular (at least that's my prediction.

I suppose try as Result<> { ... } is also ok?

Would that be semantically different than say try : Result<..> {..} ? If not, think we should reuse whatever type ascription syntax we use elsewhere.

@Manishearth
Copy link
Member

Manishearth commented Apr 4, 2018

Actually, given that try { ... } is really the combination of what most languages typically do as try {} catch {} (with the catch block kinda turned inside out), how does trycatch {} sound? Yes, it's two words, but on the plus side less folks will have used it as an identifier :)

(try_catch would also work, though that looks somewhat un-keywordy to me)

@scottmcm
Copy link
Member

scottmcm commented Apr 5, 2018

With Fail having built-in downcast, I think catch is wrong for the "this is where errors can happen" block. I could imagine a try { ... } catch e @ io::Error { } showing up in the future if "do some stuff then handle errors by downcasting from Fail" becomes the common pattern.

@clarfonthey
Copy link
Contributor

Thank you @Centril for responding so quickly! I definitely agree with your points and will probably respond more in-depth later when I'm not super tired.

Regarding requiring a type for try, I think that's best as a clippy lint than an actual requirement. I think that if it's actually required, enough people will probably start doing try: _ or try _ instead of try Result<_, _> because it'd presumably get very tiring to write. I'd rather allow omitting it but heavily suggest using it than actually requiring it, because people will probably feel more annoyed than helped. Plus, I honestly think that let x: Type = try { ... } is better than forcing a let x = try: Type { ... } anyway.

Regarding @scottmcm's comment on downcasting, I personally feel like encouraging downcasting at the language level is really not the best idea; I actually have a lot of more specific feelings about the Error trait and failure's interpretation of it, although that's a discussion for outside this RFC specifically.

@Ixrec
Copy link
Contributor

Ixrec commented Apr 5, 2018

I think we probably do want the ability to specify the type that will be caught -- in fact, I almost would go so far as to say we want to make it mandatory to specify that type. The problem is that if you don't specify it, and it winds up not being constrained from somewhere else, then you get errors because we don't know what to coerce ? into.

Does it need to be part of the try { ... } syntax though? I figured we'd just do this:

let first_two: Option<_> = try {
    let (first, rest) = self.split_first()?;
    let (second, rest) = rest.split_first()?;
    (first, second, rest)
};

and if we can't infer the type of a try block, the error message should explicitly suggest putting let name: Type = in front of the try.

@Centril
Copy link
Contributor Author

Centril commented Apr 5, 2018

@Manishearth

Actually, given that try { ... } is really the combination of what most languages typically do as try {} catch {} (with the catch block kinda turned inside out), how does trycatch {} sound? Yes, it's two words, but on the plus side less folks will have used it as an identifier :)

I would not do that for a few reasons:

  1. try { .. } catch { .. } being added after we have try { .. } is not an unreasonable prospect, it could increase ergonomics to directly match (handle) the error trapped by try.

  2. there is no precedent for a two-word keyword or the form <word><word>. The Rust convention for composite words is <word>_<word>.

  3. Furthermore, both the version without and with the underscore are unbrief compared to try.

@clarcharr

Regarding @scottmcm's comment on downcasting, I personally feel like encouraging downcasting at the language level is really not the best idea; I actually have a lot of more specific feelings about the Error trait and failure's interpretation of it, although that's a discussion for outside this RFC specifically.

Continuing the side-discussion (purely because it is interesting), I would feel comfortable with some form of downcast-pattern-match if it was syntactically legible and also programmable:

trait Downcast<Target> {
    fn downcast(Self) -> Result<Target, Self>;
}

// Here be dragons, wild speculation ahead:
try { .. } catch {
   downcast (e : io::Error) => { .. }
   ↓ (e : io::Error) => { .. } // darn it, why can't ASCII be nicer?
   // or infix:
   e ↓ io::Error => { .. }
   e downcast io::Error => { .. }
   e _> io::Error => { .. }  // underscore (down) arrow (cast)
}

PS: Let's perhaps continue this speculation on internals?

@Ixrec

Does it need to be part of the try { ... } syntax though? I figured we'd just do this:

To me it is more of a "can" than "need" proposition, in the sense that you might want type ascription as part of your super powered toolbox instead of typing judgements on the let binding. This can be nice if you don't want to create a let binding but just pass the try { .. } expression to a function like so: receiver.react_to(try : Type { .. }).

Tho the type ascription / not discussion seems somewhat orthogonal to the keyword discussion at hand ;)

@Manishearth
Copy link
Member

Manishearth commented Apr 5, 2018

try { .. } catch { .. } being added after we have try { .. } is not an unreasonable prospect, it could increase ergonomics to directly match (handle) the error trapped by try.

Honestly I don't see this as being very likely. I'm wary of changing decisions based on very-far-future concerns.

let x = trycatch {..}; match x is pretty ergonomic already. As opposed to the raison d'Γͺtre of the try {}/catch{} block itself -- scoped result code is currently pretty unergonomic.

Furthermore, both the version without and with the underscore are unbrief compared to try.

I don't think this is a big deal at all. It's as long as continue is; and will probably see similar levels of use. Maybe more, but more typing is not a big deal. I think that's an okay tradeoff to make to get a keyword that doesn't have the potential confusions of try/catch in isolation.

there is no precedent for a two-word keyword or the form . The Rust convention for composite words is _.

We could go either way, and I'm fine with breaking precedent here, there's no pressing reason to that precedent.

If "we haven't done it before" was an argument that could be made then Rust would never have happened 😺 ; in and of itself "we haven't done it before" doesn't help.

@Manishearth
Copy link
Member

Actually, thinking about it more, we could totally reserve both try and catch, and have a try catch { ... } block! This supports your future expansion to catch-matching in a clean-ish way too.

I still prefer trycatch since I don't consider the future expansion to be likely, but others may prefer this.

@Centril
Copy link
Contributor Author

Centril commented Apr 5, 2018

Honestly I don't see this as being very likely. I'm wary of changing decisions based on very-far-future concerns.

That depends on what you mean by "very-far-future". For me, that is 10 years perhaps, but 2 years is not that far if you want a robust design that ages well.

I don't think this is a big deal at all. It's as long as continue is; and will probably see similar levels of use. Maybe more, but more typing is not a big deal. I think that's an okay tradeoff to make to get a keyword that doesn't have the potential confusions of try/catch in isolation.

I think it will see more use than continue; particularly for people who like writing longer functions. And just 'cause continue is unbrief, it does not mean that we should take this as license to be unbrief again ;)
I think it's an accumulation of factors that makes try the better choice (for me).

We could go either way, and I'm fine with breaking precedent here, there's no pressing reason to that precedent.

If "we haven't done it before" was an argument that could be made then Rust would never have happened 😺 ; in and of itself "we haven't done it before" doesn't help.

Breaking precedent should not be done lightheartedly but only if there are significant reasons to, and there are certainly very significant reasons for Rust's raison d'Γͺtre. But I don't think there are for breaking rank on try.

Actually, thinking about it more, we could totally reserve both try and catch, and have a try catch { ... } block! This supports your future expansion to catch-matching in a clean-ish way too.

I think it is technically sufficient to only reserve try to get try catch { .. } since the form $ident $ident { $($stmt)* $expr } is not legal and doubly so for try $ident { $($stmt)* $expr } which would give an error due to the keyword there.

@Manishearth
Copy link
Member

Manishearth commented Apr 5, 2018

Breaking precedent should not be done lightheartedly but only if there are significant reasons to

IMO it's the opposite, if there is no reason for a precedent aside from happenstance we should not worry about breaking precedence. Otherwise any proposal is "breaking precedence".

This is why I said "in and of itself" -- there's no actual reason we haven't had keywords like this before. One can argue that they're unwieldy/long, but that's already a point under discussion and talking about precedence doesn't bolster it.

@Centril
Copy link
Contributor Author

Centril commented Apr 5, 2018

IMO it's the opposite, if there is no reason for a precedent aside from happenstance we should not worry about breaking precedence. Otherwise any proposal is "breaking precedence".

Mine was not an argument for "TRADITION!" (Fiddler on the roof).
Precedent (from other languages, and the language itself (only single word keywords)) here is important because of consistency, familiarity => learnability, because you only have so much in the complexity budget, and so you should spend it really well (and in my opinion, not waste it on unfamiliar lexical syntax).

@Manishearth
Copy link
Member

I don't see two-word keywords as "unfamiliar", really. Yes, you don't see them often, but you also don't see keywords starting with a q often (i.e. it's as unfamiliar/inconsistent as a keyword starting with a q would be -- superficially so but not in any meaningful way).

Your comment above mentions "unfamiliar lexical syntax", so it seems you're addressing familiarity in a more general sense than just for the unfamiliarity of a two-word keyword. In that context:

trycatch actually has a point towards familiarity IMO, because both try {} and catch {} bend familiarity the wrong way, whereas trycatch is familiar enough to telegraph a rough meaning, but is still different-looking enough that folks won't be confused.

You're right that it's still unfamiliar, but we basically have a tradeoff between familiarity false positives (people misunderstanding what try/catch mean because they expect something else) and unfamiliarity false negatives (people not understanding what trycatch is because it's clearly a new construct, even if the name strongly hints a meaning). The familiarity angle isn't a clear win for try or catch IMO.

(IMO the false negatives will be less likely than the false positives, but that's me.)

@warlord500
Copy link

I don't like the idea of try being used to scope result returns. it seems me that it would definitely be easily confused with the "try" macro or nightly try trait in std lib. catch or do catch seems reasonable. the only real problem with catch is that it conflicts other languages usage of the word. it certainly doesn't interact the same way many exception based languages work

@kornelski
Copy link
Contributor

kornelski commented Apr 5, 2018

Although I'm a fan of try{}, for completeness I'd like to submit wrap {} as a candidate - it wraps result of the block in a Result/Option(Future/etc?). And it's logically related to unwrap().

@Centril
Copy link
Contributor Author

Centril commented Apr 28, 2018

@SoniEx2

remember that we'll end up adding

You shouldn't presume that we'll end up adding expr 'label?.

Personally I think it is unlikely given (at least that is my hypothesis) that expr 'fn ? would be the most likely use and then expr 'closest_try ? would be the second most likely use.
I don't think that expr 'some_block_in_the_middle ? would be terribly common, and so it is not clear that this deserves syntactic sugar.

@SoniEx2
Copy link

SoniEx2 commented Apr 28, 2018

I have to. It feels like a natural evolution. And it'll be bad if it goes your way.

I would love the ability to use it with loops.

Perhaps also expr '? for the nearest block? Maybe even expr '2? and so on also? (see also: git and HEAD^ and HEAD~2)

@mgattozzi
Copy link
Contributor

@SoniEx2 you've been providing a lot of answers saying "Don't do this" without really good reasons as to why and making a lot of assumptions as to what would be added. I suggest that if you are going to make detracting statements that you should write out a fleshed out reason as to why not that addresses the arguments made for this feature, which is just reserving the key word. Not actually using it. That will require a whole separate RFC.

@audreyality
Copy link

audreyality commented Apr 29, 2018

@mgattozzi:

this feature... just [reserves] the key word. Not actually using it. That will require a whole separate RFC.

Per @Centril's on April 10th:

The RFC actually finalizes what keyword to use for foobar { .. } as try { .. }, so it is not just about keyword reservation.

If I understand the process correctly, reserving the keyword gives downstream consumers an opportunity to convert their code before the feature stabilizes. Only one keyword will be reserved, and unless a compelling alternative appears, that keyword is try. Once the keyword is reserved, nightly is free to change do catch to try.

@SoniEx2
Copy link

SoniEx2 commented Apr 29, 2018

it increases language complexity.

is that not a good reason?

we could use labels and avoid the issue, but instead we do this, that's what I have an issue with.

@rfcbot
Copy link
Collaborator

rfcbot commented May 3, 2018

The final comment period, with a disposition to merge, as per the review above, is now complete.

@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this RFC. and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels May 3, 2018
@Centril Centril merged commit 05b989a into rust-lang:master May 3, 2018
@Centril Centril deleted the rfc/try-expr branch May 3, 2018 07:35
@Centril
Copy link
Contributor Author

Centril commented May 3, 2018

Huzzah! This RFC is now merged!

Tracking issue: rust-lang/rust#50412

@SoniEx2

This comment has been minimized.

@dtolnay dtolnay mentioned this pull request Sep 2, 2018
bmisiak added a commit to bmisiak/capnproto-rust that referenced this pull request Oct 6, 2018
Rust 2018 removes the `try!()` macro and makes `try` a reserved keyword in accordance with RFC 2388: rust-lang/rfcs#2388. On the other hand, the `?` operator was introduced way back in Rust 1.13 and is now stable.

Without the change, rustc will refuse to compile the generated code starting with the 2018 edition. While this project itself is free to use `try!()` and not switch to Rust 2018, its output files - preferably - should be compatible with both editions.

rust-lang/rust#31436
@Centril Centril added A-syntax Syntax related proposals & ideas A-error-handling Proposals relating to error handling. A-keyword Proposals relating to keywords. labels Nov 23, 2018
vhdirk pushed a commit to vhdirk/capnproto-rust that referenced this pull request May 8, 2019
Rust 2018 removes the `try!()` macro and makes `try` a reserved keyword in accordance with RFC 2388: rust-lang/rfcs#2388. On the other hand, the `?` operator was introduced way back in Rust 1.13 and is now stable.

Without the change, rustc will refuse to compile the generated code starting with the 2018 edition. While this project itself is free to use `try!()` and not switch to Rust 2018, its output files - preferably - should be compatible with both editions.

rust-lang/rust#31436
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-error-handling Proposals relating to error handling. A-keyword Proposals relating to keywords. A-syntax Syntax related proposals & ideas finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.