First-class error handling with `?` and `catch` #243

Merged
merged 10 commits into from Feb 5, 2016

Projects

None yet
@glaebhoerl
Contributor
glaebhoerl commented Sep 16, 2014 edited

After a detour through thinking about first-class checked exceptions, I've now circled back around and convinced myself that @aturon's original idea of an ? operator for propagating exceptions is actually brilliant and the perfect middle path. But I also want try..catch.

CLICKME

@netvl
netvl commented Sep 16, 2014

This is really great, but I see a potential problem. The RFC does not say anything about how to translate different errors into each other, and I think this is very important. For example, your library may work with several other libraries, each providing its own kind of error, and sometimes you would want to pass these errors to the users of your library. The most natural way is to wrap them into your own error enum, with different variants for different kinds of original errors.

But under this proposal there is no support for such patterns at all. Frankly, I don't know even in the slightest how this can be done in syntactically light and convenient way and even if it is possible in principle. Exceptions in other language do not have this problem mainly due to subtyping and having special Exception supertype, and these features are not present in Rust.

I'm afraid that this can be a very common use-case, and, unfortunatley, ? operator won't help with it at all.

@aturon
Contributor
aturon commented Sep 16, 2014
@Ericson2314
Contributor

Ah, I was just thinking of making an RFC for "functional break" -- exactly your generalized return!

I am leery about most proposed syntactic sugars -- especially one as major as try..catch, and rather instead focus on using adding macros, or extending the macro system as necessary to give us what we want. (Hell, ideally I'd make bool an library-defined (enum) type, and even plain if..else a macro).

What I'd propose that people might actually agree with is simply adding the generalized break/return (Ideally one keyword could do everything, and the other would just be kept around for convenience), and making the try macro take an optional extra argument for a block lifetime:

match 'a: {
    try!('a, foo());
    try!('a, bar());
    Ok(()) // edit: added this so it would type check
} {
    None => ...,
     _ => ()
}

Not quite as pretty as try..catch, but on the other hand requires one only small addition to the language---and one that I'd argue more "rounds out" current features, since we already have loops as opposed to some system of mandatory tail calls, rather then delving it into new territory.

Besides my ascetic aversion to much syntactic sugar, I wonder whether the current demand for more control constructs will change with HKTs, and the design patterns they enable ;), and would vote for waiting until until we know the answer.

@P1start P1start and 3 others commented on an outdated diff Sep 17, 2014
active/0000-trait-based-exception-handling.md
+the `try` and `catch` blocks must therefore unify. Unlike other languages, only
+a single type of exception may be thrown in the `try` block (a `Result` only has
+a single `Err` type); and there may only be a single `catch` block, which
+catches all exceptions. This dramatically simplifies matters and allows for nice
+properties.
+
+There are two variations on the `try`..`catch` theme, each of which is more
+convenient in different circumstances.
+
+ 1. `try { EXPR } catch IRR-PAT { EXPR }`
+
+ For example:
+
+ try {
+ foo()?.bar()?
+ } catch e {
@P1start
P1start Sep 17, 2014 Contributor

Can’t e just be an arbitrary refutable pattern here, and allow multiple catch arms? So the example below becomes this:

try {
    foo()?.bar()?
} catch Red(rex) {
    baz(rex)
} catch Blue(bex) {
    quux(bex)
}

That way you only need one type of catch block and there’s less rightward drift.

@Ericson2314
Ericson2314 Sep 17, 2014 Contributor

To me that syntax looks like if-let..else, and implies no guarantee of catching all cases. Catch should handle all variants---the alternative gives me bad memories of Java's RuntimeException.

@P1start
P1start Sep 17, 2014 Contributor

Just make it like match—all cases have to be covered. Just because it looks vaguely like ‘iflet’ doesn’t mean it has to behave like it. I’d immediately assume that all errors have to be handled anyway, regardless of syntax.

@glaebhoerl
glaebhoerl Oct 3, 2014 Contributor

I think this is also a reasonable choice. But I think I prefer the match-like syntax because it better matches the actual behavior and meaning, i.e. it makes the reader think of match, rather than of try-catch-catch from other languages, and this is in fact the correct intuition. The alternative would repurpose familiar syntax to mean something similar but significantly different, which I'm a little bit uneasy about.

(But again I think both are basically fine, I'm just explaining my preference.)

@Razican
Razican Oct 4, 2015

IMHO, if something should behave like match, it should look like match.

@nrc nrc assigned brson and aturon and unassigned brson Sep 18, 2014
@aturon
Contributor
aturon commented Oct 3, 2014

@glaebhoerl

First of all, thanks for writing another beautiful and thorough RFC. It's always a pleasure to read these.

The ideas you're proposing help overcome one of my worries with the initial ? proposal, namely that it was tied to exiting the function. While this is the common case in Rust code today (probably due, in part, to try!), and you can always factor your code into smaller functions to make it work, the extra flexibility in this RFC seems both appealing and not very complicated.

I also wholeheartedly agree that this design finds a middle ground between completely implicit propagation (traditional exceptions) and completely explicit propagation (Result without try!). It recovers much of the ergonomics of implicit propagation, while keeping control-flow jumps explicit.

I think that throw and throws are promising as well. My main worry there is that throws somewhat muddies the integration with return types, for better or for worse. I suspect it could and should be integrated with closure type syntax, if we decide to include it.

I'm undecided on the Carrier question, in part because we're (still!) trying to finalize conventions around the use of Result and Option.

Have you looked at the error interoperation RFC? Part of the goal there was to allow different error types to be automatically converted when using try!. The reason that works, however, is that the function's return type (and hence the target error type) is always explicitly written. This property would no longer hold with try-catch blocks, so I'm not sure how or whether automatic conversion would apply there. I'd be interested to get your thoughts.

So, on the whole I'm pretty enthusiastic about these ideas, and I think that if we add ?, we should do so in the form proposed here.

However, as you know, at the moment we're setting a pretty high standard for RFC acceptance: we're relentlessly focusing on what is needed for a solid 1.0 release (e.g., backcompat hazards or proposals that are needed for overall usability or coherence for 1.0). After the release, we will of course start considering more "nice-to-have" features.

Personally, I feel that having a solid error-handling story is an important aspect of 1.0, which is part of why I've been pushing on various related aspects (both conventions and sugar). It's not clear to me whether try! is enough to make a good first impression, but we also have our hands rather full implementing already-accepted features. It might be possible to accept this RFC but explicitly not as a 1.0 blocker; I'm not sure, but I'll discuss it with the team.

Two final questions:

  • If we have a ? operator as proposed here for error handling, does that change your opinion about !? What if, independently, macro syntax was changed to use @ (which has been separately proposed; I know you're not fond of that, but hypothetically?)

  • Do you have thoughts on the minimal steps we can take pre-1.0 to ensure that we can implement this feature later? The main issue seems to be the try keyword, which of course conflicts with the try! macro. It's probably feasible (if confusing/ugly) to allow both, i.e. to treat ! as part of the identifier. Alternatively, we could rename try! -- any suggestions?

    Actually, one possibility would be to implement ? as it was originally proposed (i.e., limited to returning from the current function) and then dump or rename try!. Rust code written in that style would continue to feel idiomatic if/when the rest of this RFC was implemented.

(As an aside, Standard ML at least has a form of try/catch that yields an expression.)

@nikomatsakis
Contributor

OK, so I read this over. This is pretty cool stuff. If I'm not mistaken, the throws syntax (and the Carrier trait) are backwards compatible extensions, right? If so, I'd probably prefer to move slowly and leave those out for now.

There are some things I really like about this proposal:

It means that the role of the try keyword is more analogous to try as traditionally used (it defines the scope of error-handling, which try! obviously doesn't do).

This also seems to give you roughly all the things you might want to do in a fairly compact way:

  • foo? --> try!(foo)
  • try foo?.bar --> foo.map(bar)
  • try foo?.bar? --> foo.and_then(bar)
@nikomatsakis
Contributor

@aturon points out that for 1.0 staging we can add ? as a synonym for try! for now, and add try-catch keywords in later. This is probably worth nothing in the RFC.

@aturon
Contributor
aturon commented Oct 3, 2014

This also seems to give you roughly all the things you might want to do in a fairly compact way:

  • foo? --> try!(foo)
  • try foo?.bar --> foo.map(bar)
  • try foo?.bar? --> foo.and_then(bar)

Note that, in particular, this notation subsumes:

  • do notation when applied to the Error monad,
  • Swift's ?
  • try! that returns to the function boundary, which is not part of either of the two above.

I believe that even if we added monadic notation at some later time (which has many problems of its own), we would still profit from this specialized syntax for propagating and catching errors in a very lightweight way -- and a way that largely matches expectations when coming from a wide variety of other languages.

@nikomatsakis
Contributor

Also, I think I vaguely prefer the multiple catch arm syntax that @P1start suggested, since it resembles what other languages do, and because it unifies the two cases, though it obviously resembles match "less".

@arcto
arcto commented Oct 3, 2014

I really like the semantics and the ergonomics that this proposal would bring.

However, I can see this being used for a lot more than just exceptional failures. So I'm a bit unsure about the terminology and the naming of some of the constructs.

@glaebhoerl
Contributor

@Ericson2314 Feel free to submit a proposal for "functional break" if you like! I'm working on other things at the moment. (As discussed on discourse it seems like break would be the more appropriate choice, rather than return as I chose here.)


@aturon Thank you.

I think that throw and throws are promising as well. My main worry there is that throws somewhat muddies the integration with return types, for better or for worse. I suspect it could and should be integrated with closure type syntax, if we decide to include it.

Can you sketch how it might be implemented? I haven't thought about it deeply, but I wouldn't be surprised if it turned out to require higher-rank types for full generality. It's also not clear to me what the use case would be. (If you have a concrete fn that throws, it's polymorphic in the carrier, so it can be used at a closure type returning any concrete carrier. When or why would you want the closure itself to be polymorphic in the carrier?)

I'm undecided on the Carrier question, in part because we're (still!) trying to finalize conventions around the use of Result and Option.

Er, sorry: which Carrier question?

Have you looked at the error interoperation RFC? Part of the goal there was to allow different error types to be automatically converted when using try!. The reason that works, however, is that the function's return type (and hence the target error type) is always explicitly written. This property would no longer hold with try-catch blocks, so I'm not sure how or whether automatic conversion would apply there. I'd be interested to get your thoughts.

I have read it. With regards to the specific question as phrased, I suspect that the appropriate type for the try block can usually be inferred from the contents of the catch block. Do you have contrary examples? (But it's also not clear to me when you would want to do this -- wrapping in Box<Error> is something you would do to hide the specific error types used by upstream dependencies from downstream clients. But that's if you are propagating the errors. If you're handling them yourself with try..catch, why wouldn't you just inspect them directly? Why would you want to hide their types from yourself?)

Perhaps more importantly, the design as formulated in this RFC would preclude that kind of automatic conversion happening with the ? operator. Personally, I think that's a good thing. I recognize that the ergonomics of interfacing different errors types are important, but I would be deeply uncomfortable with baking this kind of ad-hoc special casing into the guts of the language. It's not even clear that this is the best way to do it, and having strong laws and ability to reason about code is much more important. ("Those who would give up essential Guarantees, to purchase a little temporary Convenience, deserve neither Guarantees nor Convenience." --Ben Franklin) I think a reasonable path forward would be to have a macro, separate from ?, which does do the automatic conversion (for instance rethrow!; I'm not sure if that's the best name). Then ? would have nice properties, the ergonomics of error-conversion would not be worse than proposed in the error interoperation RFC (which also assumed a macro), and it would be clear where automatic conversion may or may not happen.

However, as you know, at the moment we're setting a pretty high standard for RFC acceptance: we're relentlessly focusing on what is needed for a solid 1.0 release [...]

That's fine; this RFC was primarily intended to inform the ongoing debate about error handling. (But as far as I'm aware, an accepted RFC also does not imply that it has to, or will be, implemented before 1.0.)

Do you have thoughts on the minimal steps we can take pre-1.0 to ensure that we can implement this feature later?

I agree that the best approach would be to replace the try! macro with an ? operator restricted to the Result type and returning from the current function. This is a strict subset of the functionality described by the RFC, so I don't think it requires modifying the RFC in any way; it can just be considered "partially implemented" at that point.

If we have a ? operator as proposed here for error handling, does that change your opinion about !? What if, independently, macro syntax was changed to use @ (which has been separately proposed; I know you're not fond of that, but hypothetically?)

It doesn't. My opinion about the ! operator is that it's "probably a bad idea". (It has a kind of nice symmetry with ?, but that doesn't make it wise.) More importantly however, I think that adding it would be premature and that doing so is not supported by the weight of the available evidence. The idea that convention-following APIs would be too cumbersome to use without it is highly speculative, especially given that other ergonomically significant features, such as in this RFC, are not yet available. There is also at least one indication that it is possible to avoid the need for it entirely. If, after half a year or so, experience shows that living without ! is still too painful, then a compelling case could possibly be made. But not now. (And I would be surprised.)

The fact that it also entails losing the ! syntax for macros is just anti-icing on the anti-cake.

(If we decide that we don't want an ! operator after all, can we then have it back for macros? My guess is no, because having two equivalent syntaxes is undesirable.)


@nikomatsakis

try foo?.bar --> foo.map(bar)
try foo?.bar? --> foo.and_then(bar)

I can't help but notice that you left off the braces. It would be nice to allow this, but would it not run into the same ambiguities as if..else? (This might also be connected to the choice of catch syntax.)

Also, I think I vaguely prefer the multiple catch arm syntax that @P1start suggested, since it resembles what other languages do, and because it unifies the two cases, though it obviously resembles match "less".

As in my comment to @P1start, I think the fact that it unifies the cases is nice, but having similar things look similar and different things look different feels more important.


@arcto

However, I can see this being used for a lot more than just exceptional failures. So I'm a bit unsure about the terminology and the naming of some of the constructs.

I do think we can dispense with some of the stigmas and mythology surrounding exception handling in other languages, e.g. the hair-splitting about the meaning of "truly exceptional circumstances" and so on, which are due to the fact that they have to choose between error codes and exceptions, while here they are unified, and that their exceptions have various significant and undesirable aspects (e.g. not being tracked in the type system), while these don't. It can be used as just another control flow construct, and I think that's fine. Familiar syntax is still worthwhile. (See also e.g. enum for ADTs.)

@reem reem commented on an outdated diff Oct 7, 2014
active/0000-trait-based-exception-handling.md
+ fn foo(arg; Foo) -> Bar throws Baz { ... }
+
+would do two things:
+
+ * Less importantly, it would make the function polymorphic over the
+ `Result`-like type used to "carry" exceptions.
+
+ * More importantly, it means that instead of writing `return Ok(foo)` and
+ `return Err(bar)` in the body of the function, one would write `return foo`
+ and `throw bar`, and these are implicitly embedded as the "success" or
+ "exception" value in the carrier type. This removes syntactic overhead from
+ both "normal" and "throwing" code paths and (apart from `?` to propagate
+ exceptions) matches what code might look like in a language with native
+ exceptions.
+
+(This could potentially be extended to allow writing `throws` clauses on `fn`
@reem
reem Oct 7, 2014

I actually don't think we can have one without the other - it would make using throws in a declaration a very leaky abstraction if I had to find out the expanded type if I wanted to store that function anywhere, which is especially true for closures.

@reem reem commented on the diff Oct 7, 2014
active/0000-trait-based-exception-handling.md
+
+ 3. `try { EXPR }`
+
+ In this case the `try` block evaluates directly to a `Result`-like type
+ containing either the value of `EXPR`, or the exception which was thrown.
+ For instance, `try { foo()? }` is essentially equivalent to `foo()`.
+ This can be useful if you want to coalesce *multiple* potential exceptions -
+ `try { foo()?.bar()?.baz()? }` - into a single `Result`, which you wish to
+ then e.g. pass on as-is to another function, rather than analyze yourself.
+
+## (Optional) `throw` and `throws`
+
+It is possible to carry the exception handling analogy further and also add
+`throw` and `throws` constructs.
+
+`throw` is very simple: `throw EXPR` is essentially the same thing as
@reem
reem Oct 7, 2014

I want to like this a lot (it's much cleaner syntactically) but I fear the additional (apparent, if not real) complexity.

@reem reem commented on an outdated diff Oct 7, 2014
active/0000-trait-based-exception-handling.md
+ This can be useful if you want to coalesce *multiple* potential exceptions -
+ `try { foo()?.bar()?.baz()? }` - into a single `Result`, which you wish to
+ then e.g. pass on as-is to another function, rather than analyze yourself.
+
+## (Optional) `throw` and `throws`
+
+It is possible to carry the exception handling analogy further and also add
+`throw` and `throws` constructs.
+
+`throw` is very simple: `throw EXPR` is essentially the same thing as
+`Err(EXPR)?`; in other words it throws the exception `EXPR` to the innermost
+`try` block, or to the function's caller if there is none.
+
+A `throws` clause on a function:
+
+ fn foo(arg; Foo) -> Bar throws Baz { ... }
@reem
reem Oct 7, 2014

Looking at the expanded type below, I think this is a lot nicer and probably worth including if we go down this route.

@hatahet
hatahet commented Oct 7, 2014

I came across a couple of articles and thought they might be worth considering:

I realize though this is not 100% similar to C++'s (unchecked) or Java's (checked) exceptions.

@brson
Contributor
brson commented Oct 8, 2014

While I think this is an interesting and novel proposal, I am concerned about re-purposing the exception handling terminology ('try', 'catch', 'throw', 'exception').

  • Exception handling has baggage, and people will draw conclusions based on the words alone.
  • The mechanism is different from typical exceptions, has different performance characteristics and behavior.
  • Rust already also includes much of the traditional exception handling mechanism, but in a non-traditional form, under a different name (panic), so calling something else 'exceptions' makes the issue rather muddy.
  • Finally, this is such a cool idea that we might want to completely own it.
@suhr
suhr commented Oct 8, 2014

Will it make a mess when HKT is added to the language?

@nikomatsakis
Contributor

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

@brson interesting point. I wonder what would be a compelling alternative set of terms.

@arthurprs

I second @brson thoughts. If this gets incorporated it'd be best to move away from "exception handling"-ish descriptions and stick with "error handling"

@huonw
Member
huonw commented Oct 8, 2014

I can't help but notice that you left off the braces. It would be nice to allow this, but would it not run into the same ambiguities as if..else? (This might also be connected to the choice of catch syntax.)

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

I'm missing something here: don't we require braces because it would be if cond expr else, that is, there's two adjacent expressions and that doesn't work. For try (AIUI) it is just try expr catch i.e. one expression and so perfectly OK from a grammar stand-point?

Also, it's not that inconsistent with if etc; we don't require delimiters around the condition of a if/while, or match head.

@arcto
arcto commented Oct 8, 2014

I'm not going to defend this position, but short-circuiting by using an early break/return or ?-operator can be seen as a more general feature than both error handling and exception handling. Suppose, for instance, that the "carrier" is an Option. A value of None is not necessarily an error.

@glaebhoerl
Contributor

@huonw IIRC we require braces to avoid the common mistake from other C-like languages where an else clause is interpreted by the compiler as belonging to a different if than which the author intended.

@brson

  • Exception handling has baggage, and people will draw conclusions based on the words alone.

This is a reasonable point. I was thinking of aiming for the title of "the language that finally got exceptions right".

The goal of this naming scheme is to provide a good intuition for the constructs. "It works like try-catch in other languages, except you need an explicit question mark to propagate". (Whereas otherwise it could end up being "that weird error handling construct that Rust has".) I think there's enough variety among exception handling implementations in existing languages that our deviations can fit under the same roof. (Haskell also uses exception handling terminology for their implementation, which has much more in common with this one than with others.)

But even that is probably overexplaining it. They have these names because the try-catch of existing languages is where the idea came from in the first place.

(Just as a thought experiment for anyone reading -- do you think it would have been easier or harder to grok the intended meaning and usage of these proposed constructs if they had been presented using different vocabulary, not connected to exception handling?)

  • The mechanism is different from typical exceptions, has different performance characteristics and behavior.
  • Rust already also includes much of the traditional exception handling mechanism, but in a non-traditional form, under a different name (panic), so calling something else 'exceptions' makes the issue rather muddy.

I think people are more attuned to meaning than to mechanism. If they're not meant to be caught then they're not really exceptions (accordingly, we call them panics), even if it involves unwinding.

All that said this is all very theoretical. My feeling is that trying to go with different names just for the sake of being different is likely to end up being more confusing, not less. But this could be assessed much more straightforwardly for a concrete set of alternative names.

@nikomatsakis
Contributor

On Wed, Oct 08, 2014 at 06:04:41AM -0700, Huon Wilson wrote:

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

I'm missing something here: don't we require braces because it would be if cond expr else, that is, there's two adjacent expressions and that doesn't work. For try (AIUI) it is just try expr catch i.e. one expression and so perfectly OK from a grammar stand-point?

Well, there are multiple reasons to require braces. One of them is that it lets you drop the parens, yes. The other is that it avoids ambiguity for cases like

if (cond1)
    if (cond2)
        something
else
    something_else

in this case, the else is associated with the inner if, despite what the indentation suggests.

You can certainly construct analogous situations with the propose try/catch. One fix would be to permit dropping the {} if there is no catch.

@brson
Contributor
brson commented Oct 8, 2014

@glaebhoerl

(Just as a thought experiment for anyone reading -- do you think it would have been easier or harder to grok the intended meaning and usage of these proposed constructs if they had been presented using different vocabulary, not connected to exception handling?)

I do think try/catch provides the right intuition, and that's an argument that has won some (not all) naming debates in Rust in the past (and we even have several things called 'try' because it's such an intuitive concept). The word 'exception' is definitely the most worrisome here, though not having 'exceptions' but having try/catch requires some explanation.

We're probably going to end up with a long FAQ entry explaining the subtleties of exception and error handling as relates to Rust no matter what :)

@nikomatsakis
Contributor

cc @wycats, this will interest you

@wycats
Contributor
wycats commented Oct 8, 2014

@nikomatsakis thanks for tagging me in.

I'm still digesting all of this. My initial knee-jerk reaction is about terminology: try may offer some of the right intuitions, but is also drags along the intuition of "anyone downstream from me can produce an exception, and I may not be able to tell". Very few people derive their intuitions from a language with checked exceptions only, and at least for me, "exception" and try {} have a very strong intuition of unpredictability, and throwing from an arbitrary downstream.

@nikomatsakis
Contributor

I was thinking that, without the Carrier trait ideas, the role of the throw and catch keywords are unclear. That is, you can simplify the proposal to a ? operator and a try keyword that defines its scope, right? The other keywords are basically there to allow the code to be generic with respect to the "carrier" type.

@aturon
Contributor
aturon commented Oct 13, 2014

@glaebhoerl

Here are some more thoughts.

First, I think we should skip Carrier and bind directly to Result to start with. This will push people to use Result for error handling (and we also have conveniences to go from Option to Result), which is the desired convention in any case. It would keep the error handling story -- which is already novel -- as simple/concrete as possible. And we can always go more generic later, if desired.

Same with throw/throws.

Second, I agree with @brson's concerns about terminology and keyword choice -- especially given that we have unwinding with panic. Certainly, we should avoid calling this "exception handling". That said, I haven't been able to come up with a compelling alternative to try and catch, and it may be that they're the best choice despite these concerns.

Finally, I'd say that I don't particularly mind the braces for try (to me they help visually delimit the control-flow jump), and prefer having catch to trying to reuse match here (which is pretty ugly).

I'd like to move forward with this RFC soon. Does anyone have concrete proposals for alternative terminology? Does anyone think that Carrier should be committed to up front?

@Ericson2314
Contributor

I forget what Rust's status is on empty enums, absurd patterns, and all that. But a way to make an uninhabited type (let's say Void) so that Result<A, Void> is isomorphic to A would be nice in order to make to make higher-order functions that are both maximally flexible, and don't make function arguments that will never error pay for the extra dispatching on Result.

Edit: I bring it up because I suspect the carrier trait could be used to achieve the same result. Conversely if empty types will suffice, it's one less reason to have a carrier trait.

@nielsle
nielsle commented Oct 14, 2014

The ? operator looks useful, but I hope that the try! macro survives in one form or the other. The majority of my error handling follows one of the following two patterns, so I would like to avoid specifying catch blocks.

let y = try!(ctx.get_y());
try!(ctx.get_x()?.do_stuff());

If we want the user to think in terms of error propagation then I believe that rust should encourage try!and or_else instead of try {} catch {}. That would be more consistent, and I think that it most use cases (albeit not all )

It might be nice to introduce ?= ... as sugar for = try!( ... ) (But that should probably be discussed elsewhere to avoid derailing this RFC)

@glaebhoerl
Contributor

@nikomatsakis I think that's two separate questions, "why is it this way" and "how could it be different". The keywords are there for intuition and convenience (the idea of try without catch actually came much later!); the Carrier trait is there because I think it's important to support Option. But the various features are nearly all separable (just take their "deeply expanded" definitions, without reference to the other features). So yes, just try and ? is also a thing you could do. In that case you could still define some sort of polymorphic unwrap_or_else in place of catch, if you wanted to, I think. (One strange thing here is that try { foo()?.bar() } doesn't entirely mean what it says -- it's always going to "succeed" and return to its caller, just collect any errors from the body. It would actually be better suggestive to write catch { foo()?.bar() }, where you're catching any exceptions that may be thrown. But then it's difficult to explain how that fits into the broader picture of try..catch. This kind of thing may also have been what @wycats was thinking of.)

@aturon I don't mind being incremental, at all, for the reasons you state. (Though I don't think we should be making any distinctions in meaning between Option and Result beyond what is implied by their structure.)

@nielsle Sorry, it's not quite clear to me: what's the difference between ? and try! in your examples? (? as proposed in the RFC is essentially a generalization of the current try!.)

Edit: @Ericson2314: The only reason for Carrier is to be able to support both Option and Result, and it is essentially an implementation detail of the ?, try..catch, etc. constructs. User code need never interact with it directly. (It might be useful for HOFs in some way, but that wasn't the goal.)

@mitsuhiko

How did I miss this RFC? This is awesome. Much better than the comment I left in that other pull request :) I am strong +1 on the concepts in this RFC but I think that the terms try/catch should not be used because this really is not an exception system. Maybe just a variation of the words would indicate that it's something else (catch -> rescue maybe?).

I love that it supports both Option and Result but I wonder how well that works with the different rules about ignoring the return value.

I just want to point to this RFC for defaulted type parameters: #213. This is quite important if you end up in situations where you only care about the error part and the success part is generic. Without that RFC stuff like this would not work:

fn execute<T: FromSomething>(&self) -> Result<FromSomething, Error> {
    FromSomething::value_from_something(...)
}

foo.execute()?

With that RFC you could imply that T might just mean () for instance. I have this problem in my current library where people are forced to write stuff like this:

let _ : () = try!(foo.execute());
@glaebhoerl
Contributor

@mitsuhiko I don't think the particular syntax is all that important, but out of curiosity, how would you define "an exception system", and what distinguishes this from one? (There's quite a lot of variation among existing exception systems; what's the crucial aspect?)

@mitsuhiko

how would you define "an exception system", and what distinguishes this from one?

Implicitly bubbling through a stack frame that does not have to explicitly respond to it. My favorite example of how terrible this is, is Python's StopIteration. It's an exception used to indicate a failure condition (end of stream) but it can have devastating effects if it's accidentally handled in the wrong frame.

This here is explicit which I like.

@mitsuhiko

Is the throws necessary to get the return behavior? Could you not just use a general trait to convert everything that is not an error into an Ok(...) if the return value of the function is a Result?

@glaebhoerl
Contributor

It seems problematic to support returning both x and Ok(x) with equivalent meaning. (Hence throws functions return x and others return Ok(x).) I haven't delved deeply into the problems this would bring, but for example, what if one of the type arguments of the Result is itself a Result? Seems like a recipe for ambiguities and surprising behavior. (If you have a specific proposal for how to accomplish this, that might be easier to contemplate.)

@nikomatsakis
Contributor

On Tue, Oct 14, 2014 at 09:24:16AM -0700, Gábor Lehel wrote:

One strange thing here is that try { foo()?.bar() } doesn't
entirely mean what it says -- it's always going to "succeed" and
return to its caller, just collect any errors from the body. It
would actually be better suggestive to write catch { foo()? bar() }, where you're catching any exceptions that may be thrown.

I don't know that I agree. I see try as controlling the scope of
where errors are caught (in other langauges), and that is precisely
what try would do here. But anyway it's a minor point.

@bstrie
Contributor
bstrie commented Oct 15, 2014

I concur with doing our best to completely avoid the terminology of exceptions.

Overall, this feels like a lot of machinery, and comes very late in the game. I'm undecided about it.

Therefore, neither form can be considered strictly superior to the other, and it is preferable to simply provide both.

Heartily disagree. Modern Rust is veering faaar too much in the direction of TIMTOWTDI, which is a symptom of lazy design. The language needs to have the nerve to have an opinion and give me fewer options.

Same goes for the notion of brace elision. Nip that in the bud.

@arcto
arcto commented Oct 15, 2014

In advance I apologise for my infinitesimal understanding, but I'd like to be able to write something like this:

try {
    foo()?.bar()?
} on Err(e) {
    io::Error(_) => 1,
    regex::Error(_) => 2,
    on MyError(x) {
        A(_) => 3,
        B(_) => 4
    }
}
@glaebhoerl
Contributor

@arcto If I'm understanding it right, then that's essentially what I was thinking about under the name "union types". But that's a much more open-ended feature with "here be dragons" in quite a few areas, and I'm not even sure if it would be a good idea. But maybe. Future work in any case.

@reem
reem commented Oct 16, 2014

#402 addresses the above issue without unsafety, which I view as a rather large problem for effective error interoperation.

@arcto
arcto commented Oct 16, 2014

@glaebhoerl, yes, perhaps your union types or the Error trait could be incorporated into the semantics of try blocks to deal with multiple types of errors?

The other idea, also not clearly thought through, is a type catch statement that specifies the matched variant of the preceding expression: EXPR on Err(e), for example. It is meant to steer away from the terminology of exceptions (as has been asked for by several people here).

@Tobba
Tobba commented Oct 23, 2014

I really hope this doesnt get pulled

It ignores the real issue behind the whole problem and hogs a lot of syntax that could've been used for nicer things

@sfackler
Member

@Tobba Could you elaborate on what the "real issue" is and what those "nicer things" are?

@vadimcn
Contributor
vadimcn commented Oct 31, 2014

@glaebhoerl: So, why not full-on first class checked exceptions actually? What you have in the RFC is basically it, save for the ? operator. And I bet that when used en-masse, ? will be just as "invisible" as fully implicit error propagation. So why bother?

Also, we could consider using different exception propagation mechanisms depending on the calling convention, for example:

  • for "rust" functions it would be DWARF/SEH/.
  • for "rust-rc" functions, it would be via return of Result<R,E>, or something isomorphic to it.
  • "C","stdcall", etc, functions should not throw at all (i.e. anything other that throws ! is forbidden).

We could even decide that all public functions must use "rust-rc" calling convention, so there would be no cross-crate unwinding.
Likewise, embedded platforms that don't want to implement DWARF, could decide that for their target "rust" is the same as "rust-rc".

@arthurprs

The more I look at this the more I think it looks too "noisy". I think we already have a decent enough system with #201 and try! macros for 1.0. Postponing this would be a safe bet.

@glaebhoerl
Contributor

@vadimcn

  • The fact that every function would have to make an up-front decision about whether it returns an Option/Result or throws an exception, with different convenience tradeoffs for each even though they are isomorphic, bothers me. It's better to avoid having multiple parallel sets of features for accomplishing the same thing when it's possible to do so.
  • Automatic exception propagation raises the spectre of "exception safety". I did quite a bit of research into this. Essentially, in order to be able to provide the "strong guarantee" everywhere, we would have to put some kind of restriction either on try blocks (e.g. Send) or on throwing functions. It's not clear how onerous these restrictions would be, nor how bad it would be if we didn't always have the strong guarantee. (I suspect it would be much less bad than in C++, but I can't know for sure.) But it's way easier to just avoid the issue entirely.
  • It would be a more invasive language change, and optional throws clauses would have to be added to fn types, as well to the Fn traits.
  • It would likely be incredibly controversial. The only thing anyone knows is that Java has checked exceptions, and that they hate it. I looked into it, and essentially all of the problems with checked exceptions in Java which people cite are problems with Java's implementation of them, and not the concept itself, and all of them would be avoidable. But convincing everyone of this would be a bit of work.

In short I think the trait-based approach is both more politically palatable, simpler and less invasive, and all-around better on the merits.

Let me turn the question around: in what ways do you think actual checked exceptions would be superior?

@vadimcn
Contributor
vadimcn commented Nov 1, 2014

@glaebhoerl
Rust needs a first-class error handling mechanism. Result<R,E> is not a bad start, but it is clearly less than ideal, because people keep coming with macros and other syntax sugar to reduce the boilerplate of propagating errors "up".

What you propose in this RFC is not materially different from more or less traditional exceptions, except that the propagation mechanism is return-based.
I see a growing tendency to use Results for things where success is much more likely than failure. When success/failure rate is high, unwinding (or even SjLj) would be much more efficient than return based propagation.

On your bullet points:

  • If this were implemented, I imagine exceptions should completely would subsume Results. Alternatively, we could keep Result<R,E> as the return type, just like in this RFC, but for certain calling conventions, the Err variant would be propagated exclusively via unwinding, so in the "normal" code path, branches could be eliminated (along with the enum discriminator of Result).
  • Imagine compiler automatically inserted ? after every function returning Result. Would anything change in terms of required exception guarantees? I don't think so.
    What creates trouble for exception-safety are unexpected exceptions. But if all potentially throwing functions are annotated, there is no problem (at least not more than under your proposal).
  • Yes, it would have to change to Fn<ARGS, RET, ERR> / Fn(ARGS) -> RET throws ERR. Or, it could stay Fn<ARGS, Result<RET,ERR>> is we decide to represent it that way.
  • Well, let's not call them "checked exceptions" then. Let's call them, um... "ambient error monad". People like monads. :-)
@bgamari
bgamari commented Nov 1, 2014

@vadimcn the syntactic cost of propagating errors upwards should largely vanish when we have HKTs (assuming we also get do notation or some other mechanism for automatic insertion of binds). With these comes the ability to abstract over monads which have proven to be a far more flexible error handling mechanism than exceptions.

@glaebhoerl
Contributor

@vadimcn So the main reason would be performance? I guess that's valid. Although...

Alternatively, we could keep Result<R,E> as the return type, just like in this RFC, but for certain calling conventions, the Err variant would be propagated exclusively via unwinding

Couldn't we do this under this proposal as well, by the "as if" rule? Just treat anywhere which doesn't use the ? operator as a place where the exception is caught.

What creates trouble for exception-safety are unexpected exceptions. But if all potentially throwing functions are annotated, there is no problem (at least not more than under your proposal).

Possibly this is so. But in any case, having the programmer explicitly request propagation with a visual marker is even less unexpected.

Well, let's not call them "checked exceptions" then. Let's call them, um... "ambient error monad". People like monads. :-)

Yes, this is what I was going to do until I hit upon this idea instead. :) But if it looks like a duck and quacks like a duck, it's hard to avoid the idea of "a duck" forming in people's minds. Witness the deeply ambivalent reaction in this thread to the possibility of even using the same syntax and terminology as traditional exception handling.

@aturon
Contributor
aturon commented Nov 3, 2014

@glaebhoerl The core team discussed this RFC today, and while we're all enthusiastic about the design, the time remaining until the 1.0 release is so short that we don't want to try to integrate it right now. We believe that the design can be added after 1.0 backwards-compatibly, even despite the use of try in try! today. (We have a few potential avenues for resolving parsing problems there.)

That said, this proposal will really help flesh out the Rust error handling story, and we hope to come back to the RFC very quickly after the initial 1.0 release candidate.

As always, thanks for the time and effort writing another great RFC.

Closing as postponed. Tracking issue.

@aturon aturon closed this Nov 3, 2014
@aturon aturon added the postponed label Nov 3, 2014
@asb
asb commented Nov 4, 2014

Are there minutes for the meeting where this was discussed? There doesn't seem to be anything new on the meeting-minutes repo.

@glaebhoerl
Contributor

Minor note: I think the name of Carrier should be extended to ResultCarrier for a bit more clarity and a bit less intimidation. It's not meant to show up in user code anyways so verbosity doesn't matter.

@diwic
diwic commented Dec 21, 2014

While the ? thing looks like a good ergonomic improvement after a few years with the language, it is an additional thing to learn for every newcomer. Also, there is a limited number of symbols, so we might run out of symbols if we use them all for ergonomics. :-)

There are three common ways to handle a Result:

  • Panic if err -> foo.unwrap()
  • Return early if err -> try!(foo)
  • Ignore err -> foo.is_ok()

The last one seems to be largely forgotten, but relevant in several cases, e g, in destructors, if closing the resource fails, there's not much to do but to ignore the error. I tend to just add an is_ok() suffix just to get rid of the warning, but maybe we should have an explicit ignore() or drop() method instead.

It looks like you want to add ? just because try!() is too long to write, but there is also the suffix/prefix aspect, and thus if there was a potential foo.try() that returned early in case of error would be consistent enough IMO. The problem is that a function call cannot make the outer function return early. A macro however can, so how about making it possible for macros to take a "self" parameter? Then we could potentially implement a try!() function that could be called as foo.try!(). Which would also be chained as foo.try!().bar.try!(), assuming the errors are compatible.

The three ways to handle results would then look reasonably consistent:

  • foo.unwrap()
  • foo.try!()
  • foo.drop()
@Ericson2314
Contributor

let _ = ...; is AFIAK the standard idiom for ignoring a warn-unused type.

@LowLevelMahn

what about having something in between Result and Exceptions?

my ideas
http://discuss.rust-lang.org/t/maybe-better-nicer-error-handling/756

@Eilie
Eilie commented Mar 17, 2015

After using Haskell for quite a while I think "?" is silly, non general and harmful thing. Just add freaking bind operator and some syntax(do) around it plus trait to implement, and it would most general, 100% time/battle proven and extremely convenient. If you're OK with taking goodies from FP world then I don't see any reason why won't you syntatically support monads(marketing team won't let you use Monad name for that trait, but Burrito will do I think). @aturon @nikomatsakis @wycats

@huonw
Member
huonw commented Mar 17, 2015

@Ellie, closure-based sugars (like do notation) do not work well with the more interesting control flows that occur in language like Rust, e.g. supporting this is hard/impossible AFAIK:

fn foo() -> Result {
    do {
        x <- something();
        if x == 2 { return }
        wrap(x + 1)
     }
     ...
}

(where <- is bind, and wrap is Haskell Monad's return.) Based on Haskell's desugaring that would be "equivalent" to:

fn foo() -> Result {
    something().bind(|x| {
        if x == 2 { return /* somehow this has to return from `foo` */ }
        wrap(x + 1)
    })
    ....
}

There's no way for the nested return to jump out to leave foo (especially not for a general monad).

Similarly, I'm unsure how fn bar() -> Result { while x { something()?; ... } ... } would work: the compiler would have to convert the loop to a series of recursive function calls?

On the other hand, both those examples works fine with ?; the downside is of course ? can't handle arbitrary monads, essentially only error handling (which people do a lot of). I think the flexibility of ? is highly useful, even if we were to add something along the lines of do notation (I think it might be nice, but, there's quite a few problems, e.g. the ones above, so it might not work so well with Rust, in practice).

@Eilie
Eilie commented Mar 17, 2015

@huonw

In your desugared example I believe that wrap should be in separate bind so it's super easy to short circuit in user provided bind implementation if first bind returns something "wrong".

Also, such control(as you've described) flow isn't possible in Rust now either. You can't write return in function F and expect it to cause caller of F to return when F reaches its return. So return here should just return a soundly typed value from function passed to bind.

@huonw
Member
huonw commented Mar 17, 2015

wrap is not in a separate bind (at least, it's not outside the something().bind). Haskell defines do x < - y; ... to be y >>= \x -> ..., i.e. putting everything after the <- statement into the closure, which is what the bind method call is doing.

Also, such control(as you've described) flow isn't possible in Rust now either. You can't write return in function F and expect it to cause caller of F to return when F reaches its return. So return here should just return a soundly typed value from function passed to bind.

That's actually my point. :)

The "more interesting" control flow I'm talking about are just the imperative things like loops, with continue, break and return. These are currently very simple constructs in Rust, just like in C/C++/..., but making them work with closure-based sugars (i.e. allowing return from the outer function inside do notation) would require making them impossibly magical, as you say, it would require the "jump out of caller" behaviour for do { return } to do what the programmer would syntactically expect (and there's also the concerns about loops having undergo some form of CPS transformation, and it's not clear to me that such transformations will even be possible, in general). There would be a large, useful set of Rust constructs that becomes illegal inside do notation.

Haskell doesn't have the same sort of looping/jumps, so do notation works wonderfully there, without having to have lots of restrictions about what parts of the language are usable inside do.

@Eilie
Eilie commented Mar 17, 2015

@huonw Okay, got it now. I do care about Rust that's why I just want to protect it from these non general and weird built-in syntax as "?", and I'm really afraid that ? is just a start of something()[0]?#$%.bad(). Do not want Rust to be turned into write-only language with a separate built-in syntax/operator for every single specific corner case which could be solved by better abstraction like monads or whatever.

@aturon
Contributor
aturon commented Mar 17, 2015

@glaebhoerl

There is now an implementation of the most basic form of ?, thanks to the indefatigable @japaric.

There is some chance we could land that aspect of this feature and deprecate try! before 1.0, which would be nice.

I'm wondering if you'd be willing to reopen this RFC and continue the discussion?

FWIW, @nikomatsakis and I have discussed this proposal extensively and like almost all of the RFC, modulo a few minor points:

  1. The catch keyword is a bit confusing, since try always catches all exceptions. Perhaps match would make sense (but only for the multi-arm version). That would more clearly align the feature with the rest of the language.
  2. The catch e variant seems like it may be overkill (and would prevent a switch to match), since you can easily do try { .. }.or_else(|e| { ... }) and such a variant is rarely needed. So at least in the initial version, we propose dropping this variant.
  3. In terms of juggling between various error types, using something like IntoResult and always producing a Result seems the most consistent with our existing conventions, and it subsumes today's FromError.
@glaebhoerl
Contributor

@aturon Great news!

I'm wondering if you'd be willing to reopen this RFC and continue the discussion?

Of course. (Although if you were thinking of the physical act of reopening, I don't appear to have the permissions.)

With respect to those points:

  1. Yeah, this is true. A case could be made for try itself being confusing, on the same grounds: it never fails, and what it actually does is catch! (There was a bit of discussion between @nikomatsakis and myself on this point earlier above.) In either case the tension is between aligning the syntax more closely with the precedent of established languages, or with the finer points of our own semantics. I actually did consider try { ... } match { ... } while drafting the RFC (match rhymes with catch), but thought this would be too cute by half, and decided to go with catch as the more conservative option. If I had to choose today I would still opt for familiarity, but I also wouldn't get hung up on it if others feel differently.

  2. What's the basis for believing that "such a variant is rarely needed"? Honest curiosity - I included both mostly because I had (and still have) no way to anticipate their relative usefulness. If it is the case that only one of the two ends up being desired a vast majority of the time, then I agree, it is safe to drop the other. But it's not clear why we should believe so?

    But in either case and in general, I'm totally fine with adopting whichever individual parts of the RFC piecemeal in a mix-and-match fashion, regardless of the specifics here. We can always add things later if we change our minds.

  3. Are you thinking of something like the first alternative from the appendix? If not, could you lay it out in greater detail?

    I'm not very attached to particular choices of syntax, mix of features, implementation strategies, or so on, but one thing I do feel strongly about is that the desugaring (if it is a desugaring) should be predictable and non-magical. I want the (equivalent of the) laws mentioned in the RFC for the trait, as well as for the new syntax as a whole, to hold. It should be possible to reason about code using ? and try..catch without knowing the particular types and impls involved - no surprises. So this means I'm very wary of nontrivial (read as: not governed by strong laws) conversions between different error types potentially happening behind the scenes, as with the current try! macro and FromError trait. To put it very plainly, I don't think ? should be a drop-in replacement for the current try! in every case. If the error types don't match and some sort of conversion is required, then I think that should be handled separately, and ? itself should only do "dumb propagation".

    Where such conversion is desired, the current try!(foo) could be replaced by, in increasing order of specialization for convenience (and not putting any thought into names!), foo.map_err(from_error)?, with a new method foo.map_from_error()? which is defined to be the same as the preceding, or with a new macro from_error!(foo) (actually try! in a new guise).

(And what do you think about the unresolved questions still listed in the RFC itself?)

@dhardy
Contributor
dhardy commented Mar 18, 2015

@aturon using try { ... } match { ... } seems confusing to me: the match is unwrapping a value, and I don't see how it's obvious to the user that it's associated with try (mainly thinking about new-comers here). How about try { .. } handle { .. }?

@nikomatsakis
Contributor

Thinking on the choice of keyword more, I'm not sure how much to worry about user confusion. After all, if they write an exhaustive match, then they it doesn't matter what happens for cases that are not covered (there aren't any). And if they don't write an exhaustive match, they get an error -- they may be surprised to get an error, but the runtime semantics will be clear. I'd certainly prefer try/catch to try/handle, just because catch is more familiar.

@nikomatsakis
Contributor

Note that there is at least some precedent for having a single catch -- i.e., it's how JavaScript works. I forget what other langs work this way.

@aturon aturon reopened this Mar 18, 2015
@tikue
tikue commented Mar 19, 2015

If ? is added but doesn't maintain try!'s behavior with the FromError trait, I feel like there'd be a lot of code mixing try! and ?, which seems unfortunate.

@phaux
phaux commented Mar 21, 2015

De we even need the catch block? It's exactly equivalent to calling .unwrap_or_else()
As for me, try and ? operator is all I need. Example:

enum MyError { … }
impl FromError<io::Error> for MyError { … }
impl FromError<str::Utf8Error> for MyError { … }
impl FromError<json::ParserError> for MyError { … }

// open a JSON file and read a number from it
fn main() {
  let msg = try {
    let mut file = File::open("foo.json")?; // io::Error
    let mut buf: Vec<u8> = Vec::new();
    file.read_to_end(&mut buf)?; // io::Error
    let s = str::from_utf8(&buf)?; // str::Utf8Error
    let data = Json::from_str(s)?; // json::ParserError
    let num = data.as_i64().unwrap_or_default();
    format!("The number was {}", num)
  }.unwrap_or_else(|err: MyError| {
    match err {
      MyError::FileError => "File error",
      MyError::JsonError => "Json error",
      _ => "Unknown Error",
    }
  });
  println!("{}", msg);
}
@pzol
pzol commented Mar 22, 2015

+1

Piotr

On 22 Mar 2015, at 00:05, Nathan Stefaniak notifications@github.com wrote:

De we even need the catch block? It's exactly equivalent to calling .unwrap_or_else()
As for me, try and ? operator is all I need. Example:

enum MyError { … }
impl FromErrorio::Error for MyError { … }
impl FromErrorstr::Utf8Error for MyError { … }
impl FromErrorjson::ParserError for MyError { … }

// open a JSON file and read a number from it
fn main() {
let msg = try {
let mut file = File::open("foo.json")?; // io::Error
let mut buf: Vec = Vec::new();
file.read_to_end(&mut buf)?; // io::Error
let s = str::from_utf8(&buf)?; // str::Utf8Error
let data = Json::from_str(s)?; // json::ParserError
let num = data.as_i64().unwrap_or_default();
format!("The number was {}", num)
}.unwrap_or_else(|err: MyError| {
match err {
MyError::FileError => "File error",
MyError::JsonError => "Json error",
_ => "Unknown Error",
}
});
println!("{}", msg);
}

Reply to this email directly or view it on GitHub.

@Diggsey
Contributor
Diggsey commented Mar 24, 2015

This looks amazing - the only thing I dislike is having "throws" make functions generic, it potentially increases the size of generated code significantly, and it definitely increases the size of the metadata, which must now include the AST for the relevent functions.

A reasonable compromise would be to have "throws" always use "Result<T,E>" - if Option or bool is preferred then the programmer must specify it explicitly.

@aturon
Contributor
aturon commented Mar 24, 2015

@glaebhoerl

Apologies for the much-delayed response:

  1. Yeah, this is true. A case could be made for try itself being confusing, on the same grounds: it never fails, and what it actually does is catch! (There was a bit of discussion between @nikomatsakis and myself on this point earlier above.) In either case the tension is between aligning the syntax more closely with the precedent of established languages, or with the finer points of our own semantics. I actually did consider try { ... } match { ... } while drafting the RFC (match rhymes with catch), but thought this would be too cute by half, and decided to go with catch as the more conservative option. If I had to choose today I would still opt for familiarity, but I also wouldn't get hung up on it if others feel differently.

My preference here isn't terribly strong, and if we end up having multiple forms of catching as you propose, aligning with match might be more confusing than helpful.

  1. What's the basis for believing that "such a variant is rarely needed"? Honest curiosity - I included both mostly because I had (and still have) no way to anticipate their relative usefulness. If it is the case that only one of the two ends up being desired a vast majority of the time, then I agree, it is safe to drop the other. But it's not clear why we should believe so?

Two points:

  • Anecdotally, if I'm not immediately returning an error to my caller, it's because either (1) I want to match on it and do something based on its variant or (2) I want to wrap it with more information before yielding to the client. For (1), I want the full match form. For (2), it's often better done at the site generating the error (i.e. before using the ? operator)
  • Even when I do want to bind the entire error, unwrap_or_else works perfectly well in my experience. I'm not sure that the extra syntax really carries its weight. But I'm also not strongly opposed.

But in either case and in general, I'm totally fine with adopting whichever individual parts of the RFC piecemeal in a mix-and-match fashion, regardless of the specifics here. We can always add things later if we change our minds.

My sentiment is similar. In particular, I see try and ? as the core semantic parts of this proposal, and catch/match as nice sugar that we should probably experiment with separately.

  1. Are you thinking of something like the first alternative from the appendix? If not, could you lay it out in greater detail?

Yes. In particular, using generic conversion traits to convert into a Result. Note that this would subsume today's FromError.

I'm not very attached to particular choices of syntax, mix of features, implementation strategies, or so on, but one thing I do feel strongly about is that the desugaring (if it is a desugaring) should be predictable and non-magical. I want the (equivalent of the) laws mentioned in the RFC for the trait, as well as for the new syntax as a whole, to hold. It should be possible to reason about code using ? and try..catch without knowing the particular types and impls involved - no surprises. So this means I'm very wary of nontrivial (read as: not governed by strong laws) conversions between different error types potentially happening behind the scenes, as with the current try! macro and FromError trait. To put it very plainly, I don't think ? should be a drop-in replacement for the current try! in every case. If the error types don't match and some sort of conversion is required, then I think that should be handled separately, and ? itself should only do "dumb propagation".

Hm, I'm a little less sure about that. For one, the desugaring involving conversions/FromError is also very simple, and I think that FromError has by and large been a huge success in making it ergonomic to use a variety of tailored enums and other types to represent errors. I see this as well-aligned with other trends in the language, like the introduction of deref coercions. That said, I certainly appreciate the desire for laws and simple semantics.

Where such conversion is desired, the current try!(foo) could be replaced by, in increasing order of specialization for convenience (and not putting any thought into names!), foo.map_err(from_error)?, with a new method foo.map_from_error()? which is defined to be the same as the preceding, or with a new macro from_error!(foo) (actually try! in a new guise).

I personally very much want ? to subsume try!. In particular, while these more-explicit conversions could certainly work, they add to the friction one already faces when trying to use precise error hierarchies via enums, and they preclude some rather interesting ideas about abstract backtraces and the like.

FWIW, this semantics for try! has now been in place for quite some time, and I have yet to hear complaints about reasoning about it in practice, while I have heard a lot of enthusiasm for the patterns it enables.

(And what do you think about the unresolved questions still listed in the RFC itself?)

What should the precedence of the ? operator be?

I'll defer to those more familiar with the specification of Rust's grammar.

Should we add throw and/or throws?

I don't think so. In particular:

  • The polymorphism it introduces would likely wreak havoc for type inference. In particular, if you tried to use ? on the resulting type, together with a built-in use of conversions in the semantics of ?, I think type annotations would be required. I also don't find the polymorphism particularly well-motivated.
  • Polymorphism aside, I'm also not convinced that changing Err(blah)? to throw blah and return Ok(blah) to return blah is worthwhile. Both of these cases are somewhat unusual in my experience (usually I'm propagating errors, and just have a final Ok expression rather than a return), and I think the unsugared versions are more clear.

Put simply, I think throws winds up obscuring the basic mechanisms at play -- which today are pleasingly simple to understand -- without clear benefit.

Should we have impl Carrier for bool?

I don't think so. At least, I suspect I would have difficulty remembering which boolean mapped to which result, and would appreciate the clarity of being slightly more explicit in such cases. I think this is better addressed through a convenience method that clearly lifts from bools.

Should we also add the "early return from any block" feature along with this proposal, or should that be considered separately? (If we add it: should we do it by generalizing break or return?)

I would favor taking a somewhat minimal approach in this RFC, in the interest of getting this very important core feature landed ASAP. :-)

@nikomatsakis
Contributor

What should the precedence of the ? operator be?

the same as ., I think.

@Jexell
Jexell commented Mar 24, 2015

Perhaps instead of:

try { foo?; "Some string." }

It would be better to have:

try { foo?; Ok("Some string.") }

As this is more similar to functions and the current way also looks as if it has 2 "return" types.

fn bar() -> Result<&str, SomeError> {
    foo()?;
    Ok("Some string.")
}

Edit:

Polymorphism aside, I'm also not convinced that changing Err(blah)? to throw blah and return Ok(blah) to return blah is worthwhile.

Sorry @aturon, I had not noticed you had said this.

@phaux
phaux commented Mar 24, 2015

@Jexell 👍

Let's just make it work like if these were equivalent:

let result = try {
    Ok("foo")?;
    Err(1)?;
    Ok(true)
};

let result = (||{
    try!(Ok("foo"));
    try!(Err(1));
    Ok(true)
})();

And figure out how to make it detect the correct Result<bool, u8> type. I'm not sure if desugaring to a closure is a good idea, though.

Playpen

@arthurprs

Can we get some real world examples in the thread? I'm not convinced so far. try! isn't very powerful but it's clean.

@ayosec
ayosec commented Mar 25, 2015

I like this proposal. IMHO, the only downside is how visible is the ? operator as a suffix. For example, in the example in a previous comment it is hard to detect the presence of the ? (Disclaimer: I'm not an expert in Rust).

For example, instead of:

let msg = try {
  let mut file = File::open("foo.json")?;
  file.read_to_end(&mut buf)?;
  let s = str::from_utf8(&buf)?;
  let data = Json::from_str(s)?;
  // ...

It will be easier to

let msg = try {
  let mut file = File::open("foo.json").try!();
  file.read_to_end(&mut buf).try!();
  let s = str::from_utf8(&buf).try!();
  let data = Json::from_str(s).try!();
  // ...

In the RFC, the snippet

foo()?.bar()?.baz()

Will be

foo().try!().bar().try!().baz()
@dhardy
Contributor
dhardy commented Mar 26, 2015

@arthurprs this allows using try!() or ? directly in main and functions with a C interface. Currently you cannot do this, leading at least to me making some of those FFI functions simply wrap an _impl function in a few cases.

@phaux I thought the idea was to avoid returning a wrapped result? I don't really see the use of your example.

@cristicbz

It feels to me like the operator could be a separate RFC as it would introduce big ergonomic (and readabilty benefits) even without try/catch and other complex shenanigans. And it sounds like there is a lot more consensus about ? than there is about the other parts of the RFC.

Since @japaric already implemented it in rust-lang/rust#23040 it could also possibly land (feature gated) sooner.

I work with zmq a lot and the try!(try!(try!(sock.send(...)).send(...)).send(...))) is pretty darn silly compared to sock.send(...)?.send(...)?.send(...)?.

@aturon
Contributor
aturon commented Apr 8, 2015

@glaebhoerl Just wanted to check in on this -- do you think you'll have a chance to make revisions? If not, @nikomatsakis or I would be happy to make a PR against your RFC.

@glaebhoerl
Contributor

@aturon Sorry, been a bit snowed over. I'll try to make some time and effort to respond to your (and others') earlier points in the next few days. With respect to revisions, if we end up agreeing on a mutually acceptable approach, then of course - if not, I might prefer a new RFC. But that's in the future.

@aepsil0n

cc @huonw @Eilie

Regarding your earlier discussion about a more abstract monadic design, I was wondering whether it might make more sense to look towards Scala here instead of Haskell, as it is not a pure functional language but still strives to host as much functional features as possible.

Specifically I'm thinking of Scala's sequence comprehension. If we translate this concept to Rust, it could look like this:

for' x in foo(), y in x.bar() yield' y.baz()

(Using the ' here to pre-emptively avoid syntactic ambiguities here for the sake of the argument.)

While this is not terribly different from Haskell's do notation it kind of makes it more clear, that we are conceptually dealing with a sequence of pattern-expression assignments. So a statement like an early return does not really fit here. Thus it feels more natural not to use it.

And to be honest: the semantics of early returns in closures are well defined. We just would have to make it clear, that this applies in a situation like that. Plus, you don't really want to use those anyway when trying to create the kind of modular abstraction, we're talking about.

@glaebhoerl
Contributor

@aturon (So, apologies for the delay...)

I think that FromError has by and large been a huge success in making it ergonomic to use a variety of tailored enums and other types to represent errors.

For what it's worth, I agree this is also an important and worthwhile goal. Making error-handling less convenient is not necessarily my desire - I only want to make sure that while in pursuit of ergonomics, we don't unwittingly make greater sacrifices in clarity and comprehensibility. At the same time, I also don't think the prospect of a slight decrease in convenience should immediately scare us into running the other direction.

For one, the desugaring involving conversions/FromError is also very simple

That it is, but its meaning is not. What does it mean for an impl of From to be correct? Are there any properties it has to uphold? If you call from() without knowing its definition for the precise input and output types involved, what can you say about what it might or might not do, apart from what is required by the type signature? If I write struct MyInt(i32); impl From<&str> for MyInt { fn from(self) -> MyInt { MyInt(42) } } - is this completely fine? If you happen upon a bug where a given From impl does one thing, but calling code expects another, how can you determine on whose part the blame lies? These questions aren't necessarily unanswerable (and I'll make some preliminary stabs toward their answers below), but I do think we should consider it important to know the answers before we make the behavior of significant language features depend on them.

FWIW, this semantics for try! has now been in place for quite some time, and I have yet to hear complaints about reasoning about it in practice, while I have heard a lot of enthusiasm for the patterns it enables.

This is comforting to know, but I think the bar for language features should be a little bit higher. We should at least want to investigate why it seems to work, so that we can be sure it will not start to break down under greater stress or at larger scale.

I'm especially wary here because the history of automatic conversions, especially user-defined automatic conversions, counsels nothing if not wariness. The draw of convenience is strong and obvious, while the benefits of holding back are harder to grasp. The folks designing Perl, C++, and Scala surely thought it would be nicer if "obvious" conversions could be done automatically, but it seems to me that when people go down this road, they frequently come to regret it later.

The thing is that adding implicit conversions between error types seems like a minor change, but it is not. Without it, the semantics of the new constructs are obvious: it is just the Result (née Either) monad. This has useful laws which make sure that the transformations (refactorings) you would intuitively expect to be equivalent actually are. But all of that goes out the window if you throw a call to an arbitrary user-defined from() function in the middle of it. If that function can do anything, then what we can know about what ? and try might do (without knowing the definition of from()) gets closer to nothing.

But (as suggested above) perhaps it might be possible to nail down the expected behavior of from() more precisely. From what I can tell, the typical use case is to embed the values of one error type directly into another as one variant in a larger enum. Generalizing, can we say that a from() implementation should never throw any information away? Perhaps it should also not transform information in any way? But it seems harder to nail these intuitive ideas down precisely. (For instance, it seems that a from() and into() round trip should always be the identity function. If one of them e.g. does a negation (preserving all values, but transforming them) while the other does not, then this property fails. But we somehow want this to hold even if the user doesn't define both from() and into() - we should be able to assess the correctness of either independently of the other, and when only one direction is possible. How to do this is not obvious to me.) Perhaps we want some kind of homomorphism (a one-way structure preserving map), or maybe a partial isomorphism (total in one direction, partial in the other)? How can we specify the expected behavior as one or more algebraic identities (laws)?

If we succeed at that challenge, it's still only the first half of the puzzle. The second half would be to build on top of that and see if we can usefully constrain the behavior of ?, try, and catch based on what we (would now) know about the behavior of from(). Ideally, we'd be able to recover laws similar to the existing ones without from().

So I'm not outright opposed to having an implicit from()-based conversion on the error type, but I'd definitely want to have a lot more thought and care put into it before deciding to pull the trigger.

And in the meantime, the alternative is really not so bad. try!(FOO) has an overhead of 6 characters. If no conversion is required, FOO? has 1. If one is, then FOO.cast_err()? has 12 (and perhaps someone can think of a shorter, still reasonable name). You gain a few characters in one case and lose a similar amount in the other. This doesn't seem like the end of the world. Not having implicit error conversions should also be forwards compatible to adding them, and it would hopefully be good enough to tide us over while we see if the wrinkles of the latter can be satisfactorally ironed out.

(Sorry if this might've been a bit repetitive, pedantic, and/or hyperbolic in places. I don't have the capacity to refine it right now, and I've procrastinated enough.)

@glaebhoerl
Contributor

@Jexell, @phaux

That seems like an interesting idea! I'm not really sure what to think of it yet. Could you elaborate further on the differences relative to the design in the RFC, in terms of intuition, motivation, and/or specification?

@andrew-d

The draw of convenience is strong and obvious, while the benefits of holding back are harder to grasp. The folks designing Perl, C++, and Scala surely thought it would be nicer if "obvious" conversions could be done automatically, but it seems to me that when people go down this road, they frequently come to regret it later.

If nothing else, 👍 to that. There's plenty of examples of automatic conversions biting people later down the road.

@Jexell
Jexell commented Apr 20, 2015

@glaebhoerl,
Thank you! The only difference from the rfc is that the try block does not evalutate "directly to a Result-like type containing" "the value of EXPR" (given try { EXPR }). The main motivation was that if you have the code...

try {
    foo()?
}

..., the ? operator is unwrapping foo() and the try block is then immediately wrapping it back up into a Result. The other reason which I explained badly / not at all before is that in this code where foo() -> Result<&str, SomeError>...

try {
    foo()?
}

..., you are either "returning" &str and wrapping it in an Ok or "returning" Err(SomeError) but not wrapping it in an Err (unless ? differs from try! and unwraps errors).

@tikue
tikue commented Apr 21, 2015

@glaebhoerl those are very thoughtful and insightful remarks, and for the very little it's worth, I'm persuaded that the right thing to do is nail down more exact semantics for the behavior of from/into in conjunction with try/?, so that their justification is better than simply "people seem to like it."

@nrc nrc added the T-lang label May 28, 2015
@ticki
Contributor
ticki commented Jan 30, 2016

@seanmonstar that seems like a very bad idea, since it's overlapping with the potential, do sugar, for monads.

@petrochenkov
Contributor

Yay, bikeshedding.

+ to do { ... } whatever { ... }

  • do is a reserved keyword, no breakage is guaranteed and no parsing workarounds required.
  • The word do is not tied to error handling specifically, so it can denote blocks with early returns in general (which are much more practically useful than try/catch as described in the RFC):
do {
    if (condition) { break }

    /* do things */
}
  • Monadic do doesn't necessarily have to 1) use the same keyword 2) exist.

Regarding keyword for the second block, I don't have a strong opinion, however if else is chosen it has to somewhat harmonize with possible for {...} else {...} with regards to breaks, etc.


A separate FCP comment:

try/catch shouldn't be finalized/stabilized without finalizing its desugaring, i.e. blocks with early returns.

@repax
repax commented Jan 30, 2016

As a follow-up to @rpjohnst 's suggestion..

do {...} undo {...}
This pair seems to me like a natural fit for transactional semantics -- you undo what could not be done.

I've always interpreted the do keyword in functional programming as a crutch. Since the imperative style is fully available in Rust I see less of a need for such a construct, especially with that particular name.

@ticki
Contributor
ticki commented Jan 30, 2016

try/catch shouldn't be finalized/stabilized without finalizing its desugaring, i.e. blocks with early returns.

@glaebhoerl clarified: this RFC do not propose early returns.

@rpjohnst

Yes, @petrochenkov is suggesting that before try/catch (whatever it ends up being called) is stabilized, its specification be expanded to include a desugaring to early-return blocks.

As for do colliding with monads, I agree- there is currently no RFC for Haskell-style do-notation or the HKT that would be necessary to do it right. While HKT seems pretty universally supported, do-notation is a little more controversial- so while it would be nice to use the same keyword, it's not worth reserving it if it works well here.

@glaebhoerl
Contributor

Well then. Let's shed that bike.

I really don't like trap. It's alien: no other language I know of uses that word to mean anything like this. "What is this I don't even" is not a good feeling for people to have when randomly coming across a snippet of Rust code. I completely empathize with the concern that re-purposing a commonly used keyword may have misleading connotations, but there is a reason why our existing macro is called try!, and why Swift also uses the try keyword, despite it being not very similar to the exception-handling constructs in other mainstream languages, and why essentially every language with C-heritage syntax keeps reusing the same basic set of keywords (const, static, ...) to mean related but different things. The main association that "trap" suggests for me is that of CPUs trapping on an error, which has connotations of being low-level and scary and is probably not the kind of connotation we want to evoke.

I don't really like do either. It says approximately nothing at all about what it's actually doing. You see a block of imperative statements inside a do: but wait, isn't doing them what happens normally, anyways? How is this different? It doesn't say. It might suggest that it's intended to be a general abstract interpretation of sequencing like do in Haskell, but that's the wrong thing to suggest.

Both of these have the whiff of design-by-committee where something with the least strongly-defined connections to any existing thing is selected, because it's easier to achieve consensus around something that evokes a "meh" from all sides than around something that has sharper advantages and drawbacks as well.

If the drawbacks of try..catch are deemed to outweigh the advantages (and again, I think this is a completely reasonable conclusion), then my preference would be for a single-clause catch { ... } form, which both retains some familiarity, "says what it means" with much greater fidelity, and happens to fit together (on an intuitive level, even if not in actually-written code) nicely with the existing try! macro as well. If we really want to extend it to a two-clause form (as in the RFC) then we can always figure out what keyword we want to use for that later. Until then we can just use it together with the methods on Result like or_else and unwrap_or_else, or store the result in a let and then match on it if we need to use control-flow operators like break or return inside of that "second clause".

@eddyb
Member
eddyb commented Jan 30, 2016

I really like catch { ... }, defined as either stopping all returns or only those from postfix ?, in a manner similar to "break with value to labelled block".

An else clause is attractive, but is not as useful alone, so perhaps catch {...} else match {...} is an usable combination.

If we want to avoid the assert style of method, this API could work:

// Result and anything we might want to use with postfix ? (Option?).
trait Try: Sized {
    type Value;
    fn wrap(Self::Value) -> Self;
    fn try_unwrap(Self) -> Result<Self::Value, Self>;
}

// Result and other types with a success and error value.
// Option would not implement this trait (or if does, Else = ()).
// Types with more than two cases should not implement
// this trait (or if they really want to, they can set Value = Self).
trait CatchMatch: Try {
    type Else;
    fn unwrap_else(Self) -> Result<Self::Value, Self::Else>;
}

I realize using Result seems to undermine the whole movement towards a generic abstraction, but it's a really simple building block that we might as well use instead of hiding panics in our APIs and getting LLVM to optimize them away.

@aturon
Contributor
aturon commented Jan 30, 2016

FWIW, I'm pretty happy with catch as proposed here; while I understand the worry about confusion with features in other languages, the fact that it wouldn't be part of a try block is likely enough of a signal that something different is going on. Like trap, the name is somewhat suggestive of the semantics. And else match is not bad for the secondary clause.

@petrochenkov
Contributor

catch {...} else {...} reads like we hit the else block when we don't catch anything.

Something like catch {...} handle {...} is closer to what actually happen - we catch an error, then handle it, and call it all (surprise!) error handling.

(catch also looks good with breaks, so my interests are satisfied.)

@repax
repax commented Jan 30, 2016

@glaebhoerl wrote:

I don't really like do either. It says approximately nothing at all about what it's actually doing.

My intuition for do here is that it is a composition of actions that should be performed as a single unit. In this view the do {...} construct is pretty ok, and nesting them should also be obvious.

@rpjohnst

I also view do { ... } as a grouping mechanism, similar to how it's used in some Lisps. But if we don't like do, I don't see as much of a problem going back to try as long as we don't use try-catch together. So maybe try { ... } else { ... } or try { ... } else match { ... }- I actually really like the else match idea.

@logannc
logannc commented Jan 30, 2016

+1 to everything @glaebhoerl said.

I would suggest, in order:

  1. try { ... } catch { ... } - I think that languages vary enough in their error mechanisms that people will not be too surprised at the slight difference in behaviour. I mean, worst case is you have to explicitly propogate unhandled errors in your pattern matching. Error handling is so fundamental that I expect every tutorial on Rust to contain chapters on it if they hope to be complete. I think that the drawbacks are minimal and vastly overstated for what would otherwise be clearly the best choice.
  2. catch { ... } handle { ... } - this isn't my favorite because catchand handle are usually the same thing in different languages. That said, people will probably not make wrong inferences about the behaviour because they'd have to look it up. Not quite the reaction I'd like, though.
  3. catch { ... }
@logannc
logannc commented Jan 30, 2016

try { ... } else / else match { ... } is pretty good too, actually. I'd put that at 2nd. It's really hard to beat try as the first keyword.

@tikue
tikue commented Jan 30, 2016

I like catch { ... } but not else match for the same reason as @petrochenkov. I also agree that do as the first block keyword doesn't inform at all about what's happening, but perhaps as the second block keyword -- catch { ... } do { ... } it evokes the handling of the caught result.

@tikue
tikue commented Jan 30, 2016

Ohhh, sign me up for try { ... } else { ... } and try { ... } else match { ... }, the latter being shorthand when there's only one pattern.

@skinner
skinner commented Jan 30, 2016

We want to distinguish the normal/primary/expected code path from the error/failure/unusual code path, so:

normal/odd
run/trip
roll/bounce

@eddyb
Member
eddyb commented Jan 30, 2016

@tikue Actually, I would expect else to contain statements while else match a bunch of match arms so that you get access to the error value.

@logannc
logannc commented Jan 30, 2016

That's a good point. I actually like both in that case.

edit: that is, I like try { ... } else { ... } and try { ... } else match { ... }.

On Jan 30, 2016 1:13 PM, "Eduard-Mihai Burtescu" notifications@github.com
wrote:

@tikue https://github.com/tikue Actually, I would expect else to
contain statements while else match a bunch of match arms so that you get
access to the error value.


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

@tikue
tikue commented Jan 30, 2016

@eddyb yeah that's what I meant, I didn't mean to imply there'd be a single match arm, like else { e => ... }

@aturon
Contributor
aturon commented Jan 30, 2016

@logannc @tikue Please note the conflict between the meaning of try and try! -- i.e. they have nearly opposite meanings but would share the same name. As I mentioned in my earlier comment, the lang team considers try a non-starter for that reason.

@logannc
logannc commented Jan 30, 2016

By opposite meaning are you referring to the early return vs. local handling? I think that with ? superseding try!, that shouldn't be a showstopper.

@tikue
tikue commented Jan 30, 2016

@aturon By non-starter, do you mean it's non-negotiable at this point? I think there are still valid points in its favor:

  1. I think in context they'll look different enough that it won't be confusing. One introduces a block, one wraps an expression.
  2. In a vaccum (i.e. if we didn't already have try!), it is far-and-away the best keyword, almost indisputably I'd say.
  3. In the long run, ? will replace all instances of try!, thus the conflict will not matter. Rust is still very young, to the point that I think it's fair to still consider long-term best interests.

But anyway, if it's not going to happen, then it's not worth bringing up. Others had brought it up again so I assumed that meant it was still on the table to some degree.

@aturon
Contributor
aturon commented Jan 30, 2016

@tikue Sorry, to clarify, I mean that the lang team consensus was that we were unwilling to consider having both try and try! in the long run. (The RFC was originally proposed before try! was shipped in 1.0).

Whether there is a viable path toward aggressive deprecation of try! is perhaps worth talking more about. But I'll also note again that try is quite different here than in other languages, since what it really means is catch everything regardless of whether I say catch.

@rpjohnst

While try conflicts with try! and also unconditionally "catches," I also don't like catch since it feels even more tied to exceptions than try, being the corresponding word to throw, and the block itself is certainly not doing any "catching."

Another point for do- it would almost always be associated with an else or an else match, at which point it reads at least as clearly as try- "do this or else that".

@logannc
logannc commented Jan 30, 2016

If ? is accepted (which I think it will be) then we should deprecate try!. I agree with @tikue 100%.

And I think that difference will be clear because handling it requires matching which has to be exhaustive.

@eddyb
Member
eddyb commented Jan 30, 2016

@rpjohnst I really like the idea of a catch you can throw things to (within a local function), especially if it's with return. Although that might become problematic given the possibility of early returns in existing code, mixed with try!.

To me, ? is "throw a potential error", going up to a catch block within the same function.

Whether this is "just" break out of block with value or something else, I expect it to be used for more than just ?, if at all possible, and in all cases, a value is caught.

cc @reem who (IIRC) prototyped something like this using an "IIFE" in a macro.

@repax
repax commented Jan 30, 2016

@skinner wrote:

We want to distinguish the normal/primary/expected code path from the error/failure/unusual code path

By convention, catch {...} clearly reads "failure path", and it carries the baggage of exceptions. In contrast, do {...} stands out as a success path.

@burdges
burdges commented Jan 31, 2016

I suppose run would make a good keyword for either monad comprehension, or do notation. And it's not a horrible replacement for try if this business never makes it all the way to monads.

I found @m4rw3r's blog post kinda overstated because there are monads similar to error handling that seem to span the range of monad flow control : Anything that does transactions either in memory or elsewhere could roll back the transactions. Anything that must count errors and fail when things get too bad, which maintains state btw. And anything that does automatic retrys in failure situations, which mixes imperative loops and recursion/loops created by the monad.

As an aside, there is an intermediate position to named block exit where one could ask if try { .. try { .. foo()?.bar()?.baz()? .. } .. } should work when bar() returns to the type of the outer try and foo() and baz() return the type of the inner try. I think @aturon already rejected that #243 (comment) In a pinch, one can recover it by judiciously impling Into and rethrowing.

Oh. Just in case "monad comprehension" is not clear, an expression try { (x?, y?) } using this RFC could be written [ (x0,y0) | x0 <- x, y0 <- y] using monad comprehension in Haskell.

@stevenblenkinsop

By convention, catch {...} clearly reads "failure path", and it carries the baggage of exceptions. In contrast, do {...} stands out as a success path.

"Catch" is also a reactive verb, since you can only catch if there's something to catch. Since the construct guards against flow escaping, rather than being invoked only when something does attempt to escape, the meaning of the word doesn't line up with the meaning of the construct. Since the meaning of the construct also differs from the meaning of catch in other languages, it isn't recommended by either language or convention. catch-else obviously doesn't make any sense, so it would have to be catch-match.

"Trap" on the other hand, can be preventative. You can trap something without it trying to escape, or you can trap an area in anticipation of the possibility of something escaping. This seems to describe what's happening here. There's no convention of it being used on a language level, but that's neither here nor there. It does have low level connotations though. It would have to be trap-match (or trap-merge as I suggested).

do-else and do-else match can easily be construed as accurately describing what's happening: "Do this whole thing, else that." However, do on its own is not in any way illuminating about what's happening in that case, unless you think of it as building a monadic computation. Unfortunately, a monadic do would not be compatible with this construct, since it would have to rule out imperative flow control, which this proposal doesn't. I think monadic do is likely a non-starter as a first-class construct in Rust (it could easily be a macro, though), because of this incompatibility with imperative control flow, unless Rust develops strong support for non-local returns. This problem is the reason why Rust switched to external iterators way back, so it's not a given that this could ever happen.

@suhr
suhr commented Jan 31, 2016

Oh. Just in case "monad comprehension" is not clear, an expression try { (x?, y?) } using this RFC could be written [ (x0,y0) | x0 <- x, y0 <- y] using monad comprehension in Haskell.

Btw, [ (x0,y0) | x0 <- x, y0 <- y] is just return (!x, !y) in Idris.

So ? operator can be useful even with monadic do-blocks. Unlike Idris, better to make it work only inside do-blocks.

@burdges
burdges commented Jan 31, 2016

Interesting. I suppose ? here is quite similar to ! in Idris, but as stated the interpretation of ? is limited to the type of the ambient function or try block. Is this part of the interest in feature gating try separately?

As an aside, Idris is might be a good guide to monad transformers of collections too, as Idris is also strict. As I said up thread, monad transformers might not appear very much in Rust, but worth mentioning.

@glaebhoerl
Contributor

There does seem to be a close parallel between Idris's ! and our ?! (As I already wondered somewhere upthread...) I'm not sure about the prospects of generalization, though. For one thing there's the matter where unlike Idris, Rust already has an ambient monad, or not even just one: inside of fn bodies, break, continue, and return feel like some restricted subset of a continuation monad, while across fn calls, it's "only" an IO monad (i.e. Rust doesn't have non-local returns, which is what's fundamentally driving this ongoing duplication of functionality into parallel universes of functional and procedural constructs, as @stevenblenkinsop also noted). Any generalization of ? (as well as any kind of analogue for "do notation"!) would have to grapple with this, and figure out how to integrate smoothly into Rust's existing monadic nature, instead of ignoring it and recapitulating the same structures in parallel to it, naively transplanting their formulations from non-monadic languages like Idris and Haskell. (A point I think @Ericson2314 also made.) At least with respect to the way they're formulated in Idris and Haskell, I suspect the Into conversions on the error type would also get in the way, as in instance Monad (Result e), the type e is fixed.

Some more bikeshed colors:

  • catch { ... } dispatch { ... }
  • catch { ... } dis { ... }
  • catch { ... } try { ... }
  • catch { ... } catch { ... }
@ben0x539

unwrap { ... } or else { ... }, like the method on Result<>!

I guess rewrap { ... } for the single-block form...

@logannc
logannc commented Jan 31, 2016

As mentioned previously, catch should not be the first keyword has it
is reactionary and already has existing connotations.
On Jan 31, 2016 8:21 AM, "Benjamin Herr" notifications@github.com wrote:

unwrap { ... } or else { ... }, like the method on Result<>!

I guess rewrap { ... } for the single-block form...


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

@eddyb
Member
eddyb commented Jan 31, 2016

@logannc I don't see how the proposed semantics are reactionary, unlike catch in other languages and ? and the optional second block in this proposal, it simply stops the propagation.

The exiting connotations are quite strange as far as natural language goes, because by the time a "traditional" try-catch reached the catch, the exception has already been caught and you're now (maybe) matching over its type and handling it.

Consider this: try-finally with no catch clause always catches exceptions and executes the code in the finally block. I see no reason to continue with this semantic misuse.

IMO, the arguments against not using catch because of existing languages are as strong as "we shouldn't name our syntax extension facilities macros because C's macro system is such a disaster".

@repax
repax commented Jan 31, 2016

Please consider this suggestion:

  • The do keyword signifies a composite attempt. It evaluates to a Result, indicating success or failure of the entirety of the block.
  • The second block may be added to make the match exhaustive, eliminating the need for the Result.
do {
    foo()?.bar()
} else match {
    A(a) => baz(a),
    B(b) => quux(b)
}
  • The keyword match is reused for sake of familiarity. Why invent a new one (e.g. catch)?
  • The else keyword is used so as to make clear that the Ok variant has already been dealt with.
else match { ... }

In Haskell, the do keyword introduces a block of actions that can cause effects in the real world, such as reading or writing a file, typically. I see this as a crutch. Rust is already imperative and thus has less need for such a construct -- especially with that particular name. do = imperative (obvious)

@rpjohnst

Haskell's do is usable for far more than just the IO monad- it's usable for any type with an instance of the Monad typeclass (or in Rust terms, any type that implements the Monad trait). That does include IO, but it also includes Maybe (or Option in Rust) and Either (or Result in Rust), as well as things like lists (equivalent to list comprehensions as also seen in Python, Erlang, etc.), etc.

I do, however, agree that Rust does not really need Haskell's do-notation. It has full imperative control flow already, so that cat's out of the bag, it has a very good story for iterators, and what's most needed is an equivalent to Haskell's Either monad that integrates well with imperative control flow. I also think do would be a fine keyword for that usage, since it signifies grouping together related actions that all need to be done together and that have their errors handled together.

@BurntSushi
Member

and what's most needed is an equivalent to Haskell's Either monad that integrates well with imperative control flow

I note that we have this already, in the form of try! (or ? if it lands). It abstracts case analysis, type conversion and control flow. The specific advantages of the try { ... } catch { ... } outlined in the RFC and how much weight they pull is still unclear as far as I can tell (and needs experimentation, as noted by @aturon). For instance, much of the code I've written that does error handling would likely not benefit much or at all from try ... catch. (There are perhaps spurious functions that could be removed that were only introduced as a way to contain errors.) What I do believe is true is that try!/? handle quite a lot on their own, and at least for myself, I am not left wanting much more.

FWIW, I'm against using the name do unless it has nearly the same semantics as Haskell's do, which I don't believe can happen (any time soon, because Rust doesn't have abstraction over monads).

@rpjohnst

At this point, it looks like the core team doesn't like try because it conflicts with try!, other don't want catch/trap because they conflict with exceptions/interrupts and don't express the intent they'd like, and yet others don't want do because it conflicts with Haskell.

To me, however, the missing piece of functionality that try-catch provides is actually very similar to Haskell's do-notation - grouping Result/try!'s early-out behavior into a single expression. It's not an exact match, but as we don't have even a path to HKT and as monads are a terrible match for imperative control flow anyway, I see the conceptual similarities as more important. I also see block-local returns as more important in an expression-based imperative language, for refactorability reasons.

@gkoz
gkoz commented Jan 31, 2016

Sorry for being late to this party but I'm curious: are there any proposals that would make putting additional data into errors more ergonomic?

In this example (play)

#[derive(Debug)]
pub struct FileError {
    pub file_name: PathBuf,
    pub cause: io::Error,
}

impl<'a> From<(io::Error, &'a Path)> for FileError {
    fn from(error: (io::Error, &'a Path)) -> Self {
        FileError {
            cause: error.0,
            file_name: error.1.into(),
        }
    }
}

pub fn count_lines(paths: &[&Path]) -> Result<u64, FileError> {
    let mut count = 0;
    for &path in paths {
        let file = try!(File::open(path).map_err(|err| (err, path)));
        let file = BufReader::new(file);
        for line in file.lines() {
            try!(line.map_err(|err| (err, path)));
            count += 1;
        }
    }
    Ok(count)
}

I want my custom error to contain a file name and whether it's try! or ? I need to append map_err, which is a bit noisy.

@nrc
Contributor
nrc commented Feb 1, 2016

I don't really think do is appropriate - many programmers coming to Rust will be familiar with do ... while, not do in the monadic sense (Haskell is rather niche outside of the PL world). Since there's no looping here, I think using do would be too confusing.

@rpjohnst
rpjohnst commented Feb 1, 2016

That is true- though on the other hand there is the idiom in C macros of grouping statements with do { ... } while (0), so do-else not being a loop may not be too surprising.

@burdges
burdges commented Feb 1, 2016

Another word that sounds like try is attempt. It could just be a macro for (|| { ... })() if people want to find a better color later.

@gkoz There is another accepted RFC that deals with the Error trait : https://github.com/rust-lang/rfcs/blob/master/text/0201-error-chaining.md

@rpjohnst I'm not aware of any conflict between monads and imperative flow control, including break and continue. You cannot jump out of a monad comprehension boundary, but you cannot jump out of a closure either.

If anything, the imperative flow control lets you draw a cleaner boundary between the flow control you want hidden by a monad and the flow control you want to advertise, i.e. no pesky IO monad to hack around using monad transformers.

Do you have any examples? Or situations that make the optimization problem too painful?

As an aside, Rust might wish to distinguish between monads with linear run-time, like say merely Option or a transaction monad, as opposed to a monad whose run-time depends upon something, like a collection or an error monad with retrys. Any examples based on a non-linear run-time could perhaps be dealt with by offering restrictions, like a runOnce keyword and a MonadOnce trait.

@gkoz
gkoz commented Feb 1, 2016

@burdges

@gkoz There is another accepted RFC that deals with the Error trait : https://github.com/rust-lang/rfcs/blob/master/text/0201-error-chaining.md

Sure, my example relies on that (the From impl) but try!(File::create(some_path)) from that RFC is not so convenient once you want to put the file name into the error. I'm not aware of an idiomatic way of doing that and would ideally like to avoid the map_err boilerplate.

@glaebhoerl
Contributor

@gkoz I'm not aware of any ideas for solving that problem. Do you have any?

@gkoz
gkoz commented Feb 1, 2016

@glaebhoerl I haven't given this much thought. A naive idea would be extending try! and perhaps ? to accept optional parameters, e.g.

macro_rules! try (
    ($expr:expr, $par:expr) => ({
        use error;
        match $expr {
            Ok(val) => val,
            Err(err) => return Err(From::from((err, par)))
        }
    })
)

So try!(File::create(some_path), some_path) could rely on

impl<'a> From<(io::Error, &'a Path)> for FileError

from the above example.

Finding ways to pass parameters to ? could certainly be difficult.

@birkenfeld

Is there any story regarding backtraceability of "exceptions" (i.e., an Err passed through N layers of ? application)? The introduction of a new operator would be a good point to think about language-level support for that...

@rpjohnst
rpjohnst commented Feb 1, 2016

I'm not aware of any conflict between monads and imperative flow control, including break and continue. You cannot jump out of a monad comprehension boundary, but you cannot jump out of a closure either.

If anything, the imperative flow control lets you draw a cleaner boundary between the flow control you want hidden by a monad and the flow control you want to advertise, i.e. no pesky IO monad to hack around using monad transformers.

@burdges Even within a Haskell-style do-block (as opposed to crossing its boundary), the only way to do loops is with a recursive data structure (challenging in Rust to say the least), and the only way to do break/continue is by layering another monad on top with a transformer or something, which requires a run-style function to interpret. Crossing boundaries is impossible, but it doesn't have to be (and shouldn't be) for this RFC's try-catch (whatever it ends up being called).

The goal here is not to add monad support of any kind to Rust- their only relevance is in that Result technically is one and that try-catch is reminiscent of Haskell's do-notation, so some people have wanted to "generalize" it, bringing these problems in in the process. But unless someone can come up with a way to do that while still retaining the ability to use native loops/break/continue within and across boundaries, it's not going to happen.

@aidanhs
Contributor
aidanhs commented Feb 1, 2016

Suggested bikeshed colour: catch { ... } with { ... }. Although someone coming from Java etc may find it unintuitive I think it reads naturally - "I'm going to catch anything from this block with these conditions", with particular emphasis on the implication that you end up needing match all possible errors.

@tikue
tikue commented Feb 1, 2016

@birkenfeld it'd be nice to support that, but such backtraces should be opt-in, to be consistent with rust's zero-cost abstraction philosophy. While it's possible to define one's own error type that maintains a stack trace, it would be a bit unergonomic if you want line numbers. You would have to do something like .map_err(|e| MyErr::new(e, line!()))... Perhaps ? could be expanded to support this extension backwards compatibly.

@stevenblenkinsop

Suggested bikeshed colour: catch { ... } with { ... }. Although someone coming from Java etc may find it unintuitive I think it reads naturally - "I'm going to catch anything from this block with these conditions", with particular emphasis on the implication that you end up needing match all possible errors.

Despite my objections to using catch in this way, I like this. It can also work as catch { ... } with match { ... }, leaving catch-with for a potential single value / no value case, such as for working with Option.

@tikue It's pretty straightforward add an extension trait method to Result, maybe called like .note(line!()), which you can put before each ? without too much loss of readability. The next step would be to create a syntax extension which does this for you.

@burdges
burdges commented Feb 2, 2016

Actually the bind operation for a collection monad has a natural interpretation in terms of iterators @rpjohnst so I do not understand why "the only way to do loops is with a recursive data structure". At first blush, these appear to interact okay with imperative flow control. I suppose they'll force temporary immutability due to borrowing, or else require delicate implementations, but that's not too problematic.

I think the most problematic cases I know are transactions, retrys, etc. as naively they require actually being able to undo or redo things in the "ambient monad", which appears impossible. Is that what you're talking about? Imperative programmers deal with transactional objects all the time though, and they do not expect to be able to roll back everything else in the world.

There is maybe some interpretation of this as forcing monads to "manually commute" with the "ambient monad", but regardless it should not cause trouble for imperative programmers. In fact, Idris already does stuff like this with with its effects system, so some pure functional programmers get away with it too.

I'm maybe missing something important here, but so far I'm not seeing really problematic examples. Admittedly I've ignored all the monads like state, IO, or monad transformers that'd be almost useless in Rust, but Idris frequently bypasses those too. Also, I maybe rejected jumping across monad boundaries prematurely, not sure.

I'm not suggesting that ? should wait until HTKs get sorted out, quite the opposite. It's simply that the stronger the compatibility between ? and monad comprehension, the better the odds that ? might eventually work with more complex error handling. As far as I can tell this RFC gets that right with the restriction to Result.

@rpjohnst
rpjohnst commented Feb 2, 2016

@burdges I'm not referring to bind/and_then doing loops for you in the iterator/list monad, I'm referring to writing an explicit loop inside a do block. Here's the simplest example of what I'm saying doesn't work:

do {
    for _ in 0..n {
        x <- some_result();
    }
}

And here's what that would (fail to) desugar to:

for _ in 0..n {.and_then(|_| {
    some_result().and_then(|x| {
} });

Note the completely nonsensical syntax. :P If we were to introduce some kind of sugar for an implicitly-run looping monad transformer, we could almost handle this case like this:

let loop_head;
loop_head = |i| {
    if i >= n { return Ok(()); }
    some_result().and_then(|x| {
        loop_head(i + 1)
    })
};
loop_head()

But that's what I'm referring to by "recursive data structures"- loop_head owns a closure which in turn tries to own loop_head. I believe this requires Rc or some other form of garbage collection to handle in Rust, and if we wanted to use it seriously it would also require tail recursion optimization. This sort of desugaring used to be how for loops worked, long before 1.0, and that whole mess was dropped to avoid precisely this kind of shenanigans.

And that's only for natural loops (with the addition of continue) inside the monad- it doesn't handle break, and it doesn't handle either continue or break from within the do-block for a loop outside of it, both of which are routinely used with the current try! solution in the ambient monad.

@burdges
burdges commented Feb 2, 2016

Hmm. I'm surprised it'd need Rc but yeah maybe it's worse than I'd thought. There is probably a useful dividing line that could be drawn to ensure that loops, etc. worked. We're talking about monads mostly because they're the example we know, but maybe we want something more specific.

Anyways, there are monads that feel much like error handling, and do often get used with imperative flow control, but do not fit the Carrier model mentioned in the RFC. Approaching that underbelly with HTKs more restrictive than monads is likely to give a better long-term solution.

Again this does not impact ? itself since it's syntactically compatible with the comprehensions we know about, even if zero cost abstraction forbids generating code for those particular abstractions.

@hauleth
hauleth commented Feb 2, 2016

@rpjohnst we always can use nested functions:

fn loop_head<T: Add<Output=T>>(i: T) -> Result<(), Err> {
    if i >= n { return Ok(()); }
    some_result().and_then(|x| {
        loop_head(i + 1)
    })
};
loop_head()
@matthiasbeyer

I know I'm late to the party, sorry for that. Disclaimer: I'm just a user of the language, not involved in the development of the languge itself yet. My 2 cents: I don't like the syntax of the ? operator. The idea of writing

foo()?.bar()

just looks ugly IMHO.

@gnzlbg
gnzlbg commented Feb 2, 2016

I think that, as currently worded, this language feature is not general enough for those:

  • working without libstd/libcore, and/or
  • writing their own monadic error types.

The "Generalize over Result, Option, and other result-carrying types" section proposes the ResultCarrier trait which is in my opinion a much better alternative.

Still, this whole RFC screams for HKTs, which are already hard enough, and I have concerns that this RFC might make them even "harder". As a consequence I would be wary of moving forward with this RFC before we have an RFC for HKTs anyways. Since in the mean time we already have a solution, try!, I don't think this is particularly troublesome.

I cannot but wish that this RFC will motivate those interested into developing HKTs for Rust since that will have a much wider impact than this RFC. I have the feeling that almost once a month a library is discussed in reddit where HKTs could have improved its API.

@CooCooCaCha

*Note: I'm a rust noob so take this idea with a grain of salt.

What about something like this?

Result {
    foo()?.bar()?
} match {
    // Do something with the result.
}

You could also do this

Result {
    foo()?.bar()?
} Ok(stuff) {
    // Do something with stuff
} Err(e) {
    // Do something with error.
}

This way you could make similar syntax for types other than Result

@hauleth
hauleth commented Feb 2, 2016

@CooCooCaCha it is impossible with current syntax as TypeName { /* … */ } means type construction.

@CooCooCaCha

@hauleth Ah right. What about adding a keyword?

Like:

merge Result {
    foo()?.bar()?
} match {
    // Do something with the result.
}
merge Result {
    foo()?.bar()?
} Ok(stuff) {
    // Do something with stuff
} Err(e) {
    // Do something with error.
}
@hauleth
hauleth commented Feb 2, 2016

It is more possible but I would rather wait for compiler plugins and implement that as one:

chain! foo()?.bar() {
    Ok(stuff) => /* do stuff */,
    Err(err) => /* handle error */,
}

Or similar.

@rpjohnst
rpjohnst commented Feb 2, 2016

@hauleth You could implement that simple example with a normal function rather than a closure, but then you lose the enclosing scope since function items don't have an environment. You also still have the tail recursion problem, you still can't handle break inside the do-block without a monad transformer, and it's even more impossible to handle either break or continue for loops outside the do-block.

@burdges What sort of changes would you propose to Carrier? I'm not sure I understand what you're aiming at with "more restrictive than monads," but I'm curious what benefits you're looking for.

@eddyb
Member
eddyb commented Feb 2, 2016

@gnzlbg Result is in libcore (so not using libstd is irrelevant) and you cannot replace libcore without writing your own lang items, which is what Result would become, were it used in some language feature.

@briansmith

Is the Rust team going to do any study of the potential positive and negative effects of the ? in terms of code legibility and the ability to understand code before this lands? I am not going to guess whether it is a net win or loss. It definitely isn't obvious.

One of the examples given is foo()?.bar()?.baz(), which translate to (IIUC):

try!(foo());
try!(bar());
baz();

Is this a realistic example? I think a more realistic example is:

let x = try!(foo());
let y = try!(bar(x));
baz(x, y);

It would be useful to at least amend the proposal to show how this case is handled.

@hauleth
hauleth commented Feb 2, 2016

Actually foo()?.bar()?.baz()? translates to:

try!(try!(try!(foo()).bar()).baz());

Łukasz Jan Niemier

Dnia 2 lut 2016 o godz. 20:24 Brian Smith notifications@github.com napisał(a):

Is the Rust team going to do any study of the potential positive and negative effects of the ? in terms of code legibility and the ability to understand code before this lands? I am not going to guess whether it is a net win or loss. It definitely isn't obvious.

One of the examples given is foo()?.bar()?.baz(), which translate to (IIUC):

try!(foo());
try!(bar());
baz();
Is this a realistic example? I think a more realistic example is:

let x = try!(foo());
let y = try!(bar(x));
baz(x, y);
It would be useful to at least amend the proposal to show how this case is handled.


Reply to this email directly or view it on GitHub.

@tikue
tikue commented Feb 2, 2016

Frequently you'll want to map the Err type of the last call, so it'd probably usually look like one of these:

foo()?.bar()?.baz().map_err(MyErr::from)

or

Ok(foo()?.bar()?.baz()?)
@jaredr
jaredr commented Feb 2, 2016

If people don't mind a drive-by bikeshedding, let me suggest stay { }:

  • You thought you were going to return, but in fact you're going to stay right here
  • It can mean "to steady or support", which is a good match with error handling
  • It can mean "to curb/check/postpone" as in to "stay your hand", which is a decent match with cancelling a hasty return.
  • It verbs well: "the return was stayed by this block", or "the return stays in this block"
  • It's concise
@hauleth
hauleth commented Feb 2, 2016

@tikue it is exactly what and_then is for:

foo().and_then(Bar::bar).and_then(Baz::baz).map_err(MyErr::from)

or

Ok(try!(foo().and_then(Bar::bar).and_then(Baz::baz)))

But last one doesn't make much sense.

@repax
repax commented Feb 2, 2016

@jaredr
You can still return from the function and break out of loops containing the proposed construct, so stay might be a little bit confusing as a keyword.

Incidentally, trap and catch might be misleading in the same respect. I.e. return Err(x) is not "caught".

@tikue
tikue commented Feb 2, 2016

@hauleth it's not the same; and_then requires you manually unify error types.

@burdges
burdges commented Feb 3, 2016

@hauleth We've a much more efficient implementation for Result though, or heck even for a state monad, so the situations must be distinguished somehow.

@rpjohnst Idris' effects types address similar issues, like the annoyance of monad transformers not being commutative, by being more restrictive than monads. I'd imagined it covered all the monads we're trying to avoid, but actually maybe it's close to the ones we want. It's another beast about which I know relatively little though.

I suppose a useful question is : Is there anything wrong with adding a state type to Carrier so that stored types become (State,Normal) and Exception, or maybe (State,Exception), and then ? helps the functions handle the state? I worry about this example because adding state seems like just sugar in Rust, but it's the example that comes to mind.

@suhr
suhr commented Feb 3, 2016

Maybe @edwinb or @david-christiansen will tell more about Idris effect system.

@rpjohnst
rpjohnst commented Feb 3, 2016

@burdges Ah, so it's a way to make the desugaring work in more situations? Nifty, but I would think that as far as Rust goes we would want to keep control flow the same inside, outside, and across boundaries, rather than desugaring it in some cases.

@stevenblenkinsop

@rpjohnst - Ideally, you wouldn't be able to tell the difference, and the generated code would be the same either way. I'm not familiar enough with Idris to know whether this is possible in their model, though. But I agree, the behaviour of the construct shouldn't change depending on whether support for a given type is implemented in the compiler or in a library. That's why I'm skeptical of claims that the behaviour of this construct could be unified with a generalized monadic construct as long as they don't have a detailed design backing them up.

@nikomatsakis
Contributor

Huzzah! The @rust-lang/lang subteam has decided to accept this RFC (with a few tweaks described below). Thanks everyone how participated in the rather, ahem, extended discussion (more than a year!). If you're interested in seeing a summary, we attempted to summarize the discussion in two previous comments that are still mostly complete.

Given the amount of discussion on this thread, I just want to reiterate that accepting the RFC is only the first step in the overall stability process. The next step is to open a tracking issue and begin work on implementation. Further discussion about this feature can be had on the tracking issue. Once we are satisfied with the state of the implementation -- and in particular we feel we have resolved the various unresolved questions -- we will start a second FCP period on the tracking issue itself. This lasts for one release cycle (6 weeks). We will then make a final decision about ungating. In the case of this RFC, there are two distinct feature-gates, one for the ? operator and one for the "try-catch" functionality. Most likely those will be stabilized at different times (and of course we can always refine further, creating new feature-gate for subsets of the functionality if necessary).

Some specific issues we plan to revisit prior to stabilizing:

  • whether to extend to other types beyond Result;
  • compatibility (or lack thereof) with a more general do notation (and desirability thereof).

Now, on to the tweaks. We decided that for now we will simplify the RFC: rather than supporting the try/catch construct currently described, we'll just support a catch { ... } expression. catch will intercept any ? expressions within its body, just as try used to do. To get the equivalent of try { ... } catch { ... } functionality, then, one would do catch { ... }.unwrap_or(|v| match v { ... }).

Our reasoning was as follows:

  • This variation has been proposed at various times on the RFC thread, most recently by the RFC author.
  • Although try in many ways feels like the correct choice, the drawbacks are real:
    • opposite meaning of the try! macro -- even if we were to deprecate try!, it will be some time before we can do so (? must be stable first), and in that time try! will remain the recommended pattern for stable crates; there is already a large body of code, blog posts, documentation, etc that refers to try!, and that will only get larger. So even once we deprecate it, it will be even longer until all of those references are updated (if theythey ever are). Moreover, in the interim, both try! and try will co-exist on nightly, which could be very confusing, given that they do very different things.
    • the lingering concerns that try { } catch { } suggests automatic unwinding where none is happening
  • The catch (originally: try) keyword adds the real expressive "step up" here, the match (originally: catch) was just sugar for unwrap_or.
  • It would be easy to add further sugar in the future, once we see how catch is used (or not used) in practice.
  • There was some concern about potential user confusion about two aspects:
    • catch { } yields a Result<T,E> but catch { } match { } yields just T;
    • catch { } match { } handles all kinds of errors, unlike try/catch in other languages which let you pick and choose.

All right people, show's over, nothing to see here, move along (to the tracking issue). :)

@eddyb
Member
eddyb commented Feb 5, 2016

catch { ... }.unwrap_or(|v| match v { ... })

match catch { ... } { Ok(x) => x, Err(...) => ..., ... } might look better in some cases, fwiw.

@repax
repax commented Feb 5, 2016

Or like this:

if let Err(e) = catch {
    ...
} {
    handle(e);
}
@nikomatsakis nikomatsakis added a commit to nikomatsakis/rfcs that referenced this pull request Feb 5, 2016
@nikomatsakis nikomatsakis Merge and rename RFC #243. 9f7dc89
@logannc
logannc commented Feb 5, 2016

Well, isn't that nifty. If the catch block is big that might look odd
having ... } { ... but its sort of nice that there is nothing else
required.
On Feb 5, 2016 1:43 PM, "Eduard-Mihai Burtescu" notifications@github.com
wrote:

catch { ... }.unwrap_or(|v| match v { ... })

match catch { ... } { Ok(x) => x, Err(...) => ..., ... } might look
better in some cases, fwiw.


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

@nikomatsakis nikomatsakis referenced this pull request in rust-lang-nursery/rustup.rs Feb 5, 2016
Merged

Re-implement and test Manifestation::update #51

@glaebhoerl
Contributor

@nikomatsakis So where's the tracking issue? :)

@eddyb @logannc Wouldn't that run afoul of the same parser shenanigans which prevent using struct literals post-match? I recall some discussion upthread to that effect. I assume you could solve it by putting parentheses around the catch { }...

@nikomatsakis
Contributor

@glaebhoerl sorry, still catching up on the "paperwork" side here :) will open the tracking issue in a second.

@nikomatsakis
Contributor

Regarding catch as a match discriminant, I think it will run afoul of the rules regarding match expressions without parentheses, though the if let form would work fine (but of course is slightly different in its effect).

(I am presuming we are going to add catch as a contextual keyword; if it were a keyword from the start, we could potentially allow it as a match argument I guess.)

@nikomatsakis
Contributor

Tracking issue: rust-lang/rust#31436

@nikomatsakis nikomatsakis merged commit 2daab80 into rust-lang:master Feb 5, 2016
@nikomatsakis
Contributor

OK, merged (with some edits). If anybody sees any place that I missed a reference to try/catch or something like that, let me know.

@nikomatsakis
Contributor

See in particular: 94390a2

@aturon aturon changed the title from Trait-based exception handling to First-class error handling with `?` and `catch` Feb 5, 2016
@durka
Contributor
durka commented Feb 5, 2016

@nikomatsakis search for "try block" to find a bunch of missed replacements. And the "Laws" seem like they'll need to be rewritten.

@nrc
Contributor
nrc commented May 18, 2016

PR 33389 adds experimental support for the Carrier trait. Since it wasn't part of the original RFC, it should get a particularly close period of examination and discussion before we move to FCP (which should probably be separate to the FCP for the rest of the ? operator). If the trait is still contentious after experimentation and discussion, then we can open an RFC (the language team felt this did not need to be the default path though). See this discuss thread for more details.

@sciyoshi

I didn't see any discussion in this thread about the possibility of reserving ? as syntax for nullable types, i.e. Option. I feel like Option (and Result) are common enough that they might eventually warrant shortened syntax, like u32? rather than Option<u32>. Would that still be possible with this RFC?

@glaebhoerl
Contributor

@sciyoshi Yes, types and values have separate syntax.

@est31 est31 referenced this pull request Sep 16, 2016
Closed

Longer question marks RFC #1737

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment