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

Escape Blocks and Labels as try/catch/throw control flow building blocks #2440

Closed
wants to merge 6 commits into from

Conversation

Projects
None yet
5 participants
@phaylon
Copy link

phaylon commented May 13, 2018

Given the recent disagreements about the future of Rust error handling syntax and semantics, this RFC aims to, among other things:

  • Allow developing syntax and semantics outside of the core language.
  • Develop the additional syntax and semantics as a whole instead of feature-by-feature.
  • Allow people in favor of the exception-like syntax to use it on stable.
  • Allow non-error focused solutions to develop.

Rendered

@Ixrec

This comment has been minimized.

Copy link
Contributor

Ixrec commented May 13, 2018

As you probably gathered from my comments on the related RFCs, I share the skepticism that the various error handling sugars being discussed are a net win, and am extremely sympathetic to the motivation behind this RFC.

My only serious concern is that it's not clear to me that the holistic experimentation this RFC allows for would cover the whole design space, and thus we might end up biasing against certain designs that I currently have no idea how to evaluate. Specifically, I don't see how I'd be able to implement any of the following as macros:

  • try {} blocks that do both Ok-wrapping and Err-wrapping on all return expressions
  • try {} blocks that do Ok-wrapping, but still allow "manual" Ok()s as well
  • try {} blocks that do no wrapping on "bare" return expressions, but change whether or not throw, pass, etc do Ok-wrapping and Err-wrapping on their values
  • try {} blocks that do ?-insertion but no other kind of wrapping
  • try {} blocks that allow ? to be used on all Result-returning functions, including the last one (is this the same thing as Ok-wrapping?)
  • Make keywords like return and break jump to the innermost enclosing try {} block rather than the whole function

It looks like you can do try {} blocks that do only Ok-wrapping or only Err-wrapping, so maybe I'm just not creative enough or my type-fu isn't good enough to extend the proposal to all of these.

(though, even if I'm right and this RFC does turn out to exclude a lot of potential designs, it might still be a better approach than continuing to design these sugars by a series of discussions with no practical experimentation of a complete sugar system)

@phaylon

This comment has been minimized.

Copy link
Author

phaylon commented May 13, 2018

@Ixrec Yes, it is more aimed at the current functionality proposals.

I'm unsure how some of your examples would look like in code:

try {} blocks that do both Ok-wrapping and Err-wrapping on all return expressions

Do you mean actual returns? You could implement a macro wrapping code in a closure, but that would also inhibit all other control flow. Otherwise I agree, you'd need a separate feature for that.

try {} blocks that do Ok-wrapping, but still allow "manual" Ok()s as well

In the given scenario, something like that would work via an explicit result-setting macro. Otherwise I'm unsure how this can work. Especially when mixing Result and Option.

try {} blocks that do no wrapping on "bare" return expressions, but change whether or not throw, pass, etc do Ok-wrapping and Err-wrapping on their values

Not sure I understand this at all :) Could you clarify "return expressions" and "change" for this context?

try {} blocks that do ?-insertion but no other kind of wrapping

Unsure what ? insertion is here. A plain escape block does ? propagation but doesn't auto-wrap its result.

try {} blocks that allow ? to be used on all Result-returning functions, including the last one (is this the same thing as Ok-wrapping?)

As I understand it, yes, this would simply an Ok-wrapped block.

Make keywords like return and break jump to the innermost enclosing try {} block rather than the whole function

There is the break-with-value feature, which allows break 'escape value. If you mean break without label, I assume this can be simulated with a one-off loop wrapper in the macro. And like mentioned above, returning would need a separate feature if one doesn't want to block all other control flow with a wrapping closure.

Those could be combined in a macro though, I assume. Example:

loopy_try! { break 23; }

would translate to something like

(|| {
    loop {
        break escape {
            break 23;
        };
    }
})()

Of course the semantics change depending on where you insert things like Ok conversion, how you arrange the loop and escape and so on.

@rpjohnst

This comment has been minimized.

Copy link

rpjohnst commented May 13, 2018

I'm not sure how this solves the problem it claims to. It still stabilizes a particular mechanism that, this time, doesn't fit into any overall error handling system, and would thus continue exist alongside whatever final solution we would presumably get later.

That was fine for something like try!, because it didn't involve any new language features and its implementation could even be replaced with the final solution. escape looks to me like just another option that we wouldn't want to stabilize unless it was the final solution.

@phaylon

This comment has been minimized.

Copy link
Author

phaylon commented May 13, 2018

@rpjohnst

I'm not sure how this solves the problem it claims to. It still stabilizes a particular mechanism that, this time, doesn't fit into any overall error handling system, and would thus continue exist alongside whatever final solution we would presumably get later.

It could be a final feature. The RFC contains an example with an Err final value. There are also situations where you'd want a final search to be None. escape would still be usable in those cases, while still having some consistency of use when try is added. This is more general functionality that allows error specific syntax to be built on top of. It can also be used with break-with-value directly. So I wouldn't classify escape {} as error handling specific.

It also doesn't have any API implications, so if we end up with enough other functionality in core that this is no longer needed, it can be deprecated for better alternatives.

I'd personally keep using this with errors even if auto-wrapping try was added.

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented May 14, 2018

I agree with @rpjohnst here; this seems to be "try but without ok-wrapping", just adding an automatic label. If the goal is to be something only used behind macros for experimentation, it needs to be way uglier so that it won't ever get used directly.

Also, this would still need to reserve escape, which I'm definitely against as that's a well-known operation that's already used in popular crates, like http://docs.diesel.rs/diesel/expression_methods/trait.EscapeExpressionMethods.html#method.escape and https://docs.rs/regex/1.0.0/regex/fn.escape.html.

@scottmcm scottmcm added the T-lang label May 14, 2018

@phaylon

This comment has been minimized.

Copy link
Author

phaylon commented May 14, 2018

@scottmcm

If the goal is to be something only used behind macros for experimentation, it needs to be way uglier so that it won't ever get used directly.

It's not only for that. Like I said above, I'd personally choose it over try any time. Though that you can build try and try like things with it is a big win.

Also, this would still need to reserve escape

I'm open to different names. I also proposed catch in the RFC, but I'm not too particularly invested in any name.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented May 14, 2018

Though that you can build try and try like things with it is a big win.

As @scottmcm said, there is no technical difference between try { .. } without Ok-wrapping and escape { .. }, so it is literally a renamed try { .. } construct + an automatic label (which could also be done for 'try).

Beyond allowing for more experimentation in the ecosystem as to the final semantics of try { .. }, this doesn't seem to have much practical use. And I don't think stabilizing escape { .. } just for the sake of experimentation only to have try { .. } later makes much sense.

For more discussion, see: https://internals.rust-lang.org/t/an-alternative-proposal-to-try-catch-throw-error-handling/7474

@phaylon

This comment has been minimized.

Copy link
Author

phaylon commented May 14, 2018

@Centril

As @scottmcm said, there is no technical difference between try { .. } without Ok-wrapping and escape { .. }, so it is literally a renamed try { .. } construct + an automatic label (which could also be done for 'try).

An additional label and no wrapping by design does seem like a technical difference?

Beyond allowing for more experimentation in the ecosystem as to the final semantics of try { .. }, this doesn't seem to have much practical use.

From the RFC:

let item = escape {
    for item in items {
        let data = verify(item)?;
        if matches(data) {
            break 'escape Ok(data);
        }
    }
    Err(MyError::NotFound)
};

Similar for constructs that want to potentially have None as final value.

So, given try seems like it will contain result conversion, I believe there is use for a construct that is not error specific.

And I don't think stabilizing escape { .. } just for the sake of experimentation only to have try { .. } later makes much sense.

It makes sense to me because:

  • try looks like it will have wrapping.
  • We still aren't 100% sure yet if it will have wrapping.
  • For a non-error construct it certainly makes sense to not wrap.
  • Like I said above, it's not "just for experimentation". It just buys us a lot in that regard.
@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented May 14, 2018

An additional label and no wrapping by design does seem like a technical difference?

We can always add 'try later at our leisure, which removes that technical difference.

From the RFC:

Rewritten in terms of fail + try

let item = try {
    for item in items {
        let data = verify(item)?;
        if matches(data) { fail data; }
    }
    fail MyError::NotFound;
};

So, given try seems like it will contain result conversion, I believe there is use for a construct that is not error specific.

This seems like precisely the kind of barely-orthogonal feature set that was argued against in "Raising the bar for introducing new syntax".

For a non-error construct it certainly makes sense to not wrap.

You are certain of this, because?

Like I said above, it's not "just for experimentation". It just buys us a lot in that regard.

You say it is a lot; but I think almost all uses will boil down to be ? related.

@phaylon

This comment has been minimized.

Copy link
Author

phaylon commented May 14, 2018

@Centril

We can always add 'try later at our leisure, which removes that technical difference.

Only if try doesn't wrap. To recap, currently the difference would be:

  • try autoconverts its result value.
  • try doesn't have a label.

This seems like precisely the kind of barely-orthogonal feature set that was argued against in "Raising the bar for introducing new syntax".

I would argue that discussion was about things like throw and other future adjustments to error handling, which this proposal tries to ease up on. I prefer a general composable solution over a situation specific one.

You are certain of this, because?

What type are you wrapping with? If there's no implication of the specific situation where you'd use it, what variant of the output type will it be wrapped with? This would mean if it were to convert, it would be some general form of conversion, in other words an .into(). Since this construct is for general control flow, I don't think it makes sense to add implicit conversion to it.

You say it is a lot; but I think almost all uses will boil down to be ? related.

I'd argue then we wouldn't be reserving throw and fail. I often find myself with things like loops that search for things and need early exiting.

You've been arguing this from a point of try not doing conversions. I'm under the impression that the current plan is for it to indeed include the conversions. Are you against try auto-wrapping it's argument?

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented May 14, 2018

@phaylon The distinction between "currently" and what is possible wrt. 'try is unimportant. We can add a an automatic label 'try if we want, at any time we want, allowing you to build a lot of macros from it.

I would argue that discussion was about things like `throw and other future adjustments to error handling, which this proposal tries to ease up on. I prefer a general composable solution over a situation specific one.

Actually, the discussion started with ..= and Foo { x, y, z }.

Since this construct is for general control flow, I don't think it makes sense to add implicit conversion to it.

The macro could / would do a specific conversion for a specific set of types.

You've been arguing this from a point of try not doing conversions. I'm under the impression that the current plan is for it to indeed include the conversions. Are you against try auto-wrapping it's argument?

No; I'm for "the current plan" -- but having try { .. } which performs Ok-wrapping and escape { .. } concurrently in the language seems wasteful to me.

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented May 15, 2018

It's not only for that. Like I said above, I'd personally choose it over try any time.

To me, though, that loses the promise I thought this RFC was going to have. It takes it from general low-level control flow construct, like label-break-value, that would be there for macros to experiment and could become part of the dialectical ratchet, and instead makes it an opinionated semantic control flow construct, because accepting it would hit all the same "should it do Ok-wrapping" discussions that exist with try, just in the other direction. Really, the inclusion of the escape{} block seems to amount to an RFC for "try blocks shouldn't do Ok wrapping".

So I don't see the pro-try and anti-try camps unifying behind this.

To recap, currently the difference would be:

  • try autoconverts its result value.
  • try doesn't have a label.

But escape is still coupled to ?, so one could also make a macro like escape!(e) => 'escape: { try { e? }}. So again, I'm not convinced that escape is more fundamental. (Especially not compared to something like label-break-value, where LBV+if+loop can implement else, while, etc.)

@phaylon

This comment has been minimized.

Copy link
Author

phaylon commented May 15, 2018

@Centril

@phaylon The distinction between "currently" and what is possible wrt. 'try is unimportant. We can add a an automatic label 'try if we want, at any time we want, allowing you to build a lot of macros from it.

I disagree here, because try auto-wraps. I'm not sure why you keep skipping that part.

@scottmcm

To me, though, that loses the promise I thought this RFC was going to have. It takes it from general low-level control flow construct, like label-break-value, that would be there for macros to experiment and could become part of the dialectical ratchet, and instead makes it an opinionated semantic control flow construct, because accepting it would hit all the same "should it do Ok-wrapping" discussions that exist with try, just in the other direction. Really, the inclusion of the escape{} block seems to amount to an RFC for "try blocks shouldn't do Ok wrapping".

I disagree. This basically seems to me like you're saying "escape is too useful". But to find some closure for the argument: I'd be happy with a name like never_use_this_escape_block and wrap it in a macro itself.

I can see why you'd see this as a try discussion, but to me the discussion is more about having the functionality that we give to errors also available for other types and control flows. Like we have if and if let.

But escape is still coupled to ?, so one could also make a macro like escape!(e) => 'escape: { try { e? }}. So again, I'm not convinced that escape is more fundamental. (Especially not compared to something like label-break-value, where LBV+if+loop can implement else, while, etc.)

That try block would still be auto-wrapping and applying an error handling paradigm. [removed invalid argument]

Edit: Removed the argument about needing a more complicated macro, you're correct that e? should work.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented May 15, 2018

I disagree here, because try auto-wraps. I'm not sure why you keep skipping that part.

My point was in reference to there being a supposed important difference wrt. there being an automatic label or not. In this respect there is no technical difference; only a difference wrt. what we want / don't want.

I'd prefer if we'd build specialized solutions based on general ones, not the other way around.

Why not the other way around? It is fully possible to add a more general mechanism if we need to that works integrates well with try { .. }. but I doubt we do need a more general once we have try { .. };

More or less all the macros you've referenced in the RFC are ones that could be build on top of an Ok-wrapping try { .. } by simply breaking in the tail-expr position (thereby avoiding Ok-wrapping..); Others can just use the Ok-wrapping. Honestly, I'm having a hard time considering the great number of additional use cases that would be necessary to motivate having both escape and try in the language at the same time.

@phaylon

This comment has been minimized.

Copy link
Author

phaylon commented May 15, 2018

@Centril

More or less all the macros you've referenced in the RFC are ones that could be build on top of an Ok-wrapping try { .. } by simply breaking in the tail-expr position (thereby avoiding Ok-wrapping..); Others can just use the Ok-wrapping.

My disagreement here is that core would have the more specialized version, while you'd need a dependency for a single macro that is more general.

Honestly, I'm having a hard time considering the great number of additional use cases that would be necessary to motivate having both escape and try in the language at the same time.

I believe that to be subjective. I often have blocks resulting in None or Err that contain fallible code, resulting in lots of map_err calls that could be turned into one let result = escape { ... }; result.map_err(...). There's also the case of the last expression simply being a call to something returning an Option or Result. Do you only write a small amount of functions using ? that end in Err(...), None or returns_a_result_or_option(...)? I write those quite a lot, and all the small ones could probably be escape blocks instead.

Maybe our fundamental disagreement is because we write code differently.

@phaylon

This comment has been minimized.

Copy link
Author

phaylon commented May 15, 2018

Self-closing since there doesn't seem to be much support for this.

@phaylon phaylon closed this May 15, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.