From c62612508db617110d7cead92852f6e5e9e2272b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lehel?= Date: Tue, 16 Sep 2014 21:11:59 +0200 Subject: [PATCH 1/9] Trait-based exception handling --- active/0000-trait-based-exception-handling.md | 702 ++++++++++++++++++ 1 file changed, 702 insertions(+) create mode 100644 active/0000-trait-based-exception-handling.md diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md new file mode 100644 index 00000000000..836e0021afe --- /dev/null +++ b/active/0000-trait-based-exception-handling.md @@ -0,0 +1,702 @@ +- Start Date: 2014-09-16 +- RFC PR #: (leave this empty) +- Rust Issue #: (leave this empty) + + +# Summary + +Add sugar for working with existing algebraic datatypes such as `Result` and +`Option`. Put another way, use types such as `Result` and `Option` to model +common exception handling constructs. + +Add a trait which precisely spells out the abstract interface and requirements +for such types. + +The new constructs are: + + * An `?` operator for explicitly propagating exceptions. + + * A `try`..`catch` construct for conveniently catching and handling exceptions. + + * (Potentially) a `throw` operator, and `throws` sugar for function signatures. + +The idea for the `?` operator originates from [RFC PR 204][204] by @aturon. + +[204]: https://github.com/rust-lang/rfcs/pull/204 + + +# Motivation and overview + +Rust currently uses algebraic `enum` types `Option` and `Result` for error +handling. This solution is simple, well-behaved, and easy to understand, but +often gnarly and inconvenient to work with. We would like to solve the latter +problem while retaining the other nice properties and avoiding duplication of +functionality. + +We can accomplish this by adding constructs which mimic the exception-handling +constructs of other languages in both appearance and behavior, while improving +upon them in typically Rustic fashion. These constructs are well-behaved in a +very precise sense and their meaning can be specified by a straightforward +source-to-source translation into existing language constructs (plus a very +simple and obvious new one). (They may also, but need not necessarily, be +implemented in this way.) + +These constructs are strict additions to the existing language, and apart from +the issue of keywords, the legality and behavior of all currently existing Rust +programs is entirely unaffected. + +The most important additions are a postfix `?` operator for propagating +"exceptions" and a `try`..`catch` block for catching and handling them. By an +"exception", we more or less just mean the `None` variant of an `Option` or the +`Err` variant of a `Result`. (See the "Detailed design" section for more +precision.) + +## `?` operator + +The postfix `?` operator can be applied to expressions of types like `Option` +and `Result` which contain either a "success" or an "exception" value, and can +be thought of as a generalization of the current `try! { }` macro. It either +returns the "success" value directly, or performs an early exit and propagates +the "exception" value further out. (So given `my_result: Result`, we +have `my_result?: Foo`.) This allows it to be used for e.g. conveniently +chaining method calls which may each "throw an exception": + + foo()?.bar()?.baz() + +(Naturally, in this case the types of the "exceptions thrown by" `foo()` and +`bar()` must unify.) + +When used outside of a `try` block, the `?` operator propagates the exception to +the caller of the current function, just like the current `try!` macro does. (If +the return type of the function isn't one, like `Result`, that's capable of +carrying the exception, then this is a type error.) When used inside a `try` +block, it propagates the exception up to the innermost `try` block, as one would +expect. + +Requiring an explicit `?` operator to propagate exceptions strikes a very +pleasing balance between completely automatic exception propagation, which most +languages have, and completely manual propagation, which we currently have +(apart from the `try!` macro to lessen the pain). It means that function calls +remain simply function calls which return a result to their caller, with no +magic going on behind the scenes; and this also *increases* flexibility, because +one gets to choose between propagation with `?` or consuming the returned +`Result` directly. + +The `?` operator itself is suggestive, syntactically lightweight enough to not +be bothersome, and lets the reader determine at a glance where an exception may +or may not be thrown. It also means that if the signature of a function changes +with respect to exceptions, it will lead to type errors rather than silent +behavior changes, which is always a good thing. Finally, because exceptions are +tracked in the type system, there is no silent propagation of exceptions, and +all points where an exception may be thrown are readily apparent visually, this +also means that we do not have to worry very much about "exception safety". + +## `try`..`catch` + +Like most other things in Rust, and unlike other languages that I know of, +`try`..`catch` is an *expression*. If no exception is thrown in the `try` block, +the `try`..`catch` evaluates to the value of `try` block; if an exception is +thrown, it is passed to the `catch` block, and the `try`..`catch` evaluates to +the value of the `catch` block. As with `if`..`else` expressions, the types of +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 { + let x = baz(e); + quux(x, e); + } + + Here the caught exception is bound to an irrefutable pattern immediately + following the `catch`. + This form is convenient when one does not wish to do case analysis on the + caught exception. + + 2. `try { EXPR } catch { PAT => EXPR, PAT => EXPR, ... }` + + For example: + + try { + foo()?.bar()? + } catch { + Red(rex) => baz(rex), + Blue(bex) => quux(bex) + } + + Here the `catch` is not immediately followed by a pattern; instead, its body + performs a `match` on the caught exception directly, using any number of + refutable patterns. + This form is convenient when one *does* wish to do case analysis on the + caught exception. + +While it may appear to be extravagant to provide both forms, there is reason to +do so: either form on its own leads to unavoidable rightwards drift under some +circumstances. + +The first form leads to rightwards drift if one wishes to `match` on the caught +exception: + + try { + foo()?.bar()? + } catch e { + match e { + Red(rex) => baz(rex), + Blue(bex) => quux(bex) + } + } + +This `match e` is quite redundant and unfortunate. + +The second form leads to rightwards drift if one wishes to do more complex +multi-statement work with the caught exception: + + try { + foo()?.bar()? + } catch { + e => { + let x = baz(e); + quux(x, e); + } + } + +This single case arm is quite redundant and unfortunate. + +Therefore, neither form can be considered strictly superior to the other, and it +is preferable to simply provide both. + +Finally, it is also possible to write a `try` block *without* a `catch` block: + + 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 +`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 { ... } + +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` +and closure *types*, desugaring to a type parameter with a `Carrier` bound on +the parent item (e.g. a HOF), but this would be considerably more involved, and +it's not clear whether there is value in doing so.) + + +# Detailed design + +The meaning of the constructs will be specified by a source-to-source +translation. We make use of an "early exit from any block" feature which doesn't +currently exist in the language, generalizes the current `break` and `return` +constructs, and is independently useful. + +## Early exit from any block + +The capability can be exposed either by generalizing `break` to take an optional +value argument and break out of any block (not just loops), or by generalizing +`return` to take an optional lifetime argument and return from any block, not +just the outermost block of the function. This feature is independently useful +and I believe it should be added, but as it is only used here in this RFC as an +explanatory device, and implementing the RFC does not require exposing it, I am +going to arbitrarily choose the `return` syntax for the following and won't +discuss the question further. + +So we are extending `return` with an optional lifetime argument: `return 'a +EXPR`. This is an expression of type `!` which causes an early return from the +enclosing block specified by `'a`, which then evaluates to the value `EXPR` (of +course, the type of `EXPR` must unify with the type of the last expression in +that block). + +A completely artificial example: + + 'a: { + let my_thing = if have_thing { + get_thing() + } else { + return 'a None + }; + println!("found thing: {}", my_thing); + Some(my_thing) + } + +Here if we don't have a thing, we escape from the block early with `None`. + +If no lifetime is specified, it defaults to returning from the whole function: +in other words, the current behavior. We can pretend there is a magical lifetime +`'fn` which refers to the outermost block of the current function, which is the +default. + +## The trait + +Here we specify the trait for types which can be used to "carry" either a normal +result or an exception. There are several different, completely equivalent ways +to formulate it, which differ only in the set of methods: for other +possibilities, see the appendix. + + #[lang(carrier)] + trait Carrier { + type Normal; + type Exception; + fn embed_normal(from: Normal) -> Self; + fn embed_exception(from: Exception) -> Self; + fn translate>(from: Self) -> Other; + } + +This trait basically just states that `Self` is isomorphic to +`Result` for some types `Normal` and `Exception`. For greater +clarity on how these methods work, see the section on `impl`s below. (For a +simpler formulation of the trait using `Result` directly, see the appendix.) + +The `translate` method says that it should be possible to translate to any +*other* `Carrier` type which has the same `Normal` and `Exception` types. This +can be used to inspect the value by translating to a concrete type such as +`Result` and then, for example, pattern matching on it. + +Laws: + + 1. For all `x`, `translate(embed_normal(x): A): B ` = `embed_normal(x): B`. + 2. For all `x`, `translate(embed_exception(x): A): B ` = `embed_exception(x): B`. + 3. For all `carrier`, `translate(translate(carrier: A): B): A` = `carrier: A`. + +Here I've used explicit type ascription syntax to make it clear that e.g. the +types of `embed_` on the left and right hand sides are different. + +The first two laws say that embedding a result `x` into one carrier type and +then translating it to a second carrier type should be the same as embedding it +into the second type directly. + +The third law says that translating to a different carrier type and then +translating back should be the identity function. + + +## `impl`s of the trait + + impl Carrier for Result { + type Normal = T; + type Exception = E; + fn embed_normal(a: T) -> Result { Ok(a) } + fn embed_exception(e: E) -> Result { Err(e) } + fn translate>(result: Result) -> Other { + match result { + Ok(a) => Other::embed_normal(a), + Err(e) => Other::embed_exception(e) + } + } + } + +As we can see, `translate` can be implemented by deconstructing ourself and then +re-embedding the contained value into the other carrier type. + + impl Carrier for Option { + type Normal = T; + type Exception = (); + fn embed_normal(a: T) -> Option { Some(a) } + fn embed_exception(e: ()) -> Option { None } + fn translate>(option: Option) -> Other { + match option { + Some(a) => Other::embed_normal(a), + None => Other::embed_exception(()) + } + } + } + +Potentially also: + + impl Carrier for bool { + type Normal = (); + type Exception = (); + fn embed_normal(a: ()) -> bool { true } + fn embed_exception(e: ()) -> bool { false } + fn translate>(b: bool) -> Other { + match b { + true => Other::embed_normal(()), + false => Other::embed_exception(()) + } + } + } + +The laws should be sufficient to rule out any "icky" impls. For example, an impl +for `Vec` where an exception is represented as the empty vector, and a normal +result as a single-element vector: here the third law fails, because if the +`Vec` has more than element *to begin with*, then it's not possible to translate +to a different carrier type and then back without losing information. + +The `bool` impl may be surprising, or not useful, but it *is* well-behaved: +`bool` is, after all, isomorphic to `Result<(), ()>`. This `impl` may be +included or not; I don't have a strong opinion about it. + +## Definition of constructs + +Finally we have the definition of the new constructs in terms of a +source-to-source translation. + +In each case except the first, I will provide two definitions: a single-step +"shallow" desugaring which is defined in terms of the previously defined new +constructs, and a "deep" one which is "fully expanded". + +Of course, these could be defined in many equivalent ways: the below definitions +are merely one way. + + * Construct: + + throw EXPR + + Shallow: + + return 'here Carrier::embed_exception(EXPR) + + Where `'here` refers to the innermost enclosing `try` block, or to `'fn` if + there is none. As with `return`, `EXPR` may be omitted and defaults to `()`. + + * Construct: + + EXPR? + + Shallow: + + match translate(EXPR) { + Ok(a) => a, + Err(e) => throw e + } + + Deep: + + match translate(EXPR) { + Ok(a) => a, + Err(e) => return 'here Carrier::embed_exception(e) + } + + * Construct: + + try { + foo()?.bar() + } + + Shallow: + + 'here: { + Carrier::embed_normal(foo()?.bar()) + } + + Deep: + + 'here: { + Carrier::embed_normal(match translate(foo()) { + Ok(a) => a, + Err(e) => return 'here Carrier::embed_exception(e) + }.bar()) + } + + * Construct: + + try { + foo()?.bar() + } catch e { + baz(e) + } + + Shallow: + + match try { + foo()?.bar() + } { + Ok(a) => a, + Err(e) => baz(e) + } + + Deep: + + match 'here: { + Carrier::embed_normal(match translate(foo()) { + Ok(a) => a, + Err(e) => return 'here Carrier::embed_exception(e) + }.bar()) + } { + Ok(a) => a, + Err(e) => baz(e) + } + + * Construct: + + try { + foo()?.bar() + } catch { + A(a) => baz(a), + B(b) => quux(b) + } + + Shallow: + + try { + foo()?.bar() + } catch e { + match e { + A(a) => baz(a), + B(b) => quux(b) + } + } + + Deep: + + match 'here: { + Carrier::embed_normal(match translate(foo()) { + Ok(a) => a, + Err(e) => return 'here Carrier::embed_exception(e) + }.bar()) + } { + Ok(a) => a, + Err(e) => match e { + A(a) => baz(a), + B(b) => quux(b) + } + } + + * Construct: + + fn foo(A) -> B throws C { + CODE + } + + Shallow: + + fn foo>(A) -> Car { + try { + 'fn: { + CODE + } + } + } + + Deep: + + fn foo>(A) -> Car { + 'here: { + Carrier::embed_normal('fn: { + CODE + }) + } + } + + (Here our desugaring runs into a stumbling block, and we resort to a pun: the + *whole function* should be conceptually wrapped in a `try` block, and a + `return` inside `CODE` should be embedded as a successful result into the + carrier, rather than escaping from the `try` block itself. We suggest this by + putting the "magical lifetime" `'fn` *inside* the `try` block.) + +The fully expanded translations get quite gnarly, but that is why it's good that +you don't have to write them! + +In general, the types of the defined constructs should be the same as the types +of their definitions. + +(As noted earlier, while the behavior of the constructs can be *specified* using +a source-to-source translation in this manner, they need not necessarily be +*implemented* this way.) + +## Laws + +Without any attempt at completeness, and modulo `translate()` between different +carrier types, here are some things which should be true: + + * `try { foo() } ` = `Ok(foo())` + * `try { throw e } ` = `Err(e)` + * `try { foo()? } ` = `foo()` + * `try { foo() } catch e { e }` = `foo()` + * `try { throw e } catch e { e }` = `e` + * `try { Ok(foo()?) } catch e { Err(e) }` = `foo()` + +## Misc + + * Our current lint for unused results could be replaced by one which warns for + any unused result of a type which implements `Carrier`. + + * If there is ever ambiguity due to the carrier type being underdetermined + (experience should reveal whether this is a problem in practice), we could + resolve it by defaulting to `Result`. (This would presumably involve making + `Result` a lang item.) + + * Translating between different carrier types with the same `Normal` and + `Exception` types *should*, but may not necessarily *currently* be, a no-op + most of the time. + + We should make it so that: + + * repr(`Option`) = repr(`Result`) + * repr(`bool`) = repr(`Option<()>`) = repr(`Result<(), ()>`) + + If these hold, then `translate` between these types could in theory be + compiled down to just a `transmute`. (Whether LLVM is smart enough to do + this, I don't know.) + + * The `translate()` function smells to me like a natural transformation between + functors, but I'm not category theorist enough for it to be obvious. + + +# Drawbacks + + * Adds new constructs to the language. + + * Some people have a philosophical objection to "there's more than one way to + do it". + + * Relative to first-class checked exceptions, our implementation options are + constrained: while actual checked exceptions could be implemented in a + similar way to this proposal, they could also be implemented using unwinding, + should we choose to do so, and we do not realistically have that option here. + + +# Alternatives + + * Do nothing. + + * Only add the `?` operator, but not any of the other constructs. + + * Instead of a general `Carrier` trait, define everything directly in terms of + `Result`. This has precedent in that, for example, the `if`..`else` construct + is also defined directly in terms of `bool`. (However, this would likely also + lead to removing `Option` from the standard library in favor of + `Result<_, ()>`.) + + * Add [first-class checked exceptions][notes], which are propagated + automatically (without an `?` operator). + + This has the drawbacks of being a more invasive change and duplicating + functionality: each function must choose whether to use checked exceptions + via `throws`, or to return a `Result`. While the two are isomorphic and + converting between them is easy, with this proposal, the issue does not even + arise, as exception handling is defined *in terms of* `Result`. Furthermore, + automatic exception propagation raises the specter of "exception safety": how + serious an issue this would actually be in practice, I don't know - there's + reason to believe that it would be much less of one than in C++. + +[notes]: https://github.com/glaebhoerl/rust-notes/blob/268266e8fbbbfd91098d3bea784098e918b42322/my_rfcs/Exceptions.txt + + +# Unresolved questions + + * What should the precedence of the `?` operator be? + + * Should we add `throw` and/or `throws`? + + * Should we have `impl Carrier for bool`? + + * 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`?) + + +# Appendices + +## Alternative formulations of the `Carrier` trait + +All of these have the form: + + trait Carrier { + type Normal; + type Exception; + ...methods... + } + +and differ only in the methods, which will be given. + +### Explicit isomorphism with `Result` + + fn from_result(Result) -> Self; + fn to_result(Self) -> Result; + +This is, of course, the simplest possible formulation. + +The drawbacks are that it, in some sense, privileges `Result` over other +potentially equivalent types, and that it may be less efficient for those types: +for any non-`Result` type, every operation requires two method calls (one into +`Result`, and one out), whereas with the `Carrier` trait in the main text, they +only require one. + +Laws: + + * For all `x`, `from_result(to_result(x))` = `x`. + * For all `x`, `to_result(from_result(x))` = `x`. + +Laws for the remaining formulations below are left as an exercise for the +reader. + +### Avoid privileging `Result`, most naive version + + fn embed_normal(Normal) -> Self; + fn embed_exception(Exception) -> Self; + fn is_normal(&Self) -> bool; + fn is_exception(&Self) -> bool; + fn assert_normal(Self) -> Normal; + fn assert_exception(Self) -> Exception; + +Of course this is horrible. + +### Destructuring with HOFs (a.k.a. Church/Scott-encoding) + + fn embed_normal(Normal) -> Self; + fn embed_exception(Exception) -> Self; + fn match_carrier(Self, FnOnce(Normal) -> T, FnOnce(Exception) -> T) -> T; + +This is probably the right approach for Haskell, but not for Rust. + +With this formulation, because they each take ownership of them, the two +closures may not even close over the same variables! + +### Destructuring with HOFs, round 2 + + trait BiOnceFn { + type ArgA; + type ArgB; + type Ret; + fn callA(Self, ArgA) -> Ret; + fn callB(Self, ArgB) -> Ret; + } + + trait Carrier { + type Normal; + type Exception; + fn normal(Normal) -> Self; + fn exception(Exception) -> Self; + fn match_carrier(Self, BiOnceFn) -> T; + } + +Here we solve the environment-sharing problem from above: instead of two objects +with a single method each, we use a single object with two methods! I believe +this is the most flexible and general formulation (which is however a strange +thing to believe when they are all equivalent to each other). Of course, it's +even more awkward syntactically. From 63a3c4d088374a9f3929acd77cdaa407b6d80e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lehel?= Date: Tue, 16 Sep 2014 21:40:09 +0200 Subject: [PATCH 2/9] I was going to mention "just do it with a macro" in the Alternatives, but it somehow got lost in the shuffle --- active/0000-trait-based-exception-handling.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md index 836e0021afe..2ef64c818ac 100644 --- a/active/0000-trait-based-exception-handling.md +++ b/active/0000-trait-based-exception-handling.md @@ -353,8 +353,8 @@ Potentially also: The laws should be sufficient to rule out any "icky" impls. For example, an impl for `Vec` where an exception is represented as the empty vector, and a normal result as a single-element vector: here the third law fails, because if the -`Vec` has more than element *to begin with*, then it's not possible to translate -to a different carrier type and then back without losing information. +`Vec` has more than one element *to begin with*, then it's not possible to +translate to a different carrier type and then back without losing information. The `bool` impl may be surprising, or not useful, but it *is* well-behaved: `bool` is, after all, isomorphic to `Result<(), ()>`. This `impl` may be @@ -586,6 +586,15 @@ carrier types, here are some things which should be true: * Only add the `?` operator, but not any of the other constructs. + * Instead of a built-in `try`..`catch` construct, attempt to define one using + macros. However, this is likely to be awkward because, at least, macros may + only have their contents as a single block, rather than two. Furthermore, + macros are excellent as a "safety net" for features which we forget to add + to the language itself, or which only have specialized use cases; but after + seeing this proposal, we need not forget `try`..`catch`, and its prevalence + in nearly every existing language suggests that it is, in fact, generally + useful. + * Instead of a general `Carrier` trait, define everything directly in terms of `Result`. This has precedent in that, for example, the `if`..`else` construct is also defined directly in terms of `bool`. (However, this would likely also From ab63c4fc3a5661eb396d0fdf5852df2d9ed33b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lehel?= Date: Tue, 29 Dec 2015 22:10:42 +0100 Subject: [PATCH 3/9] RFC update, first pass: * Monomorphize to `Result`, and move all `Carrier` stuff to a "Future possibilities" section. * Move `throw` and `throws` to "Future possibilities". * Move the irrefutable-pattern form of `catch` to "Future possibilities". * Early-exit using `break` instead of `return`. * Rename `Carrier` to `ResultCarrier`. * Miscellaneous other improvements. --- active/0000-trait-based-exception-handling.md | 669 ++++++++---------- 1 file changed, 280 insertions(+), 389 deletions(-) diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md index 2ef64c818ac..202d6058593 100644 --- a/active/0000-trait-based-exception-handling.md +++ b/active/0000-trait-based-exception-handling.md @@ -5,29 +5,22 @@ # Summary -Add sugar for working with existing algebraic datatypes such as `Result` and -`Option`. Put another way, use types such as `Result` and `Option` to model -common exception handling constructs. - -Add a trait which precisely spells out the abstract interface and requirements -for such types. +Add syntactic sugar for working with the `Result` type which models common exception handling constructs. The new constructs are: - * An `?` operator for explicitly propagating exceptions. - - * A `try`..`catch` construct for conveniently catching and handling exceptions. + * An `?` operator for explicitly propagating "exceptions". - * (Potentially) a `throw` operator, and `throws` sugar for function signatures. + * A `try`..`catch` construct for conveniently catching and handling "exceptions". -The idea for the `?` operator originates from [RFC PR 204][204] by @aturon. +The idea for the `?` operator originates from [RFC PR 204][204] by [@aturon](https://github.com/aturon). [204]: https://github.com/rust-lang/rfcs/pull/204 # Motivation and overview -Rust currently uses algebraic `enum` types `Option` and `Result` for error +Rust currently uses the `enum Result` type for error handling. This solution is simple, well-behaved, and easy to understand, but often gnarly and inconvenient to work with. We would like to solve the latter problem while retaining the other nice properties and avoiding duplication of @@ -35,10 +28,9 @@ functionality. We can accomplish this by adding constructs which mimic the exception-handling constructs of other languages in both appearance and behavior, while improving -upon them in typically Rustic fashion. These constructs are well-behaved in a -very precise sense and their meaning can be specified by a straightforward -source-to-source translation into existing language constructs (plus a very -simple and obvious new one). (They may also, but need not necessarily, be +upon them in typically Rustic fashion. Their meaning can be specified by a straightforward +source-to-source translation into existing language constructs, plus a very +simple and obvious new one. (They may also, but need not necessarily, be implemented in this way.) These constructs are strict additions to the existing language, and apart from @@ -47,17 +39,15 @@ programs is entirely unaffected. The most important additions are a postfix `?` operator for propagating "exceptions" and a `try`..`catch` block for catching and handling them. By an -"exception", we more or less just mean the `None` variant of an `Option` or the -`Err` variant of a `Result`. (See the "Detailed design" section for more +"exception", we essentially just mean the `Err` variant of a `Result`. (See the "Detailed design" section for more precision.) + ## `?` operator -The postfix `?` operator can be applied to expressions of types like `Option` -and `Result` which contain either a "success" or an "exception" value, and can -be thought of as a generalization of the current `try! { }` macro. It either -returns the "success" value directly, or performs an early exit and propagates -the "exception" value further out. (So given `my_result: Result`, we +The postfix `?` operator can be applied to `Result` values and is equivalent to the current `try!()` macro. It either +returns the `Ok` value directly, or performs an early exit and propagates +the `Err` value further out. (So given `my_result: Result`, we have `my_result?: Foo`.) This allows it to be used for e.g. conveniently chaining method calls which may each "throw an exception": @@ -68,15 +58,13 @@ chaining method calls which may each "throw an exception": When used outside of a `try` block, the `?` operator propagates the exception to the caller of the current function, just like the current `try!` macro does. (If -the return type of the function isn't one, like `Result`, that's capable of -carrying the exception, then this is a type error.) When used inside a `try` +the return type of the function isn't a `Result`, then this is a type error.) When used inside a `try` block, it propagates the exception up to the innermost `try` block, as one would expect. Requiring an explicit `?` operator to propagate exceptions strikes a very pleasing balance between completely automatic exception propagation, which most -languages have, and completely manual propagation, which we currently have -(apart from the `try!` macro to lessen the pain). It means that function calls +languages have, and completely manual propagation, which we'd have apart from the `try!` macro. It means that function calls remain simply function calls which return a result to their caller, with no magic going on behind the scenes; and this also *increases* flexibility, because one gets to choose between propagation with `?` or consuming the returned @@ -86,11 +74,12 @@ The `?` operator itself is suggestive, syntactically lightweight enough to not be bothersome, and lets the reader determine at a glance where an exception may or may not be thrown. It also means that if the signature of a function changes with respect to exceptions, it will lead to type errors rather than silent -behavior changes, which is always a good thing. Finally, because exceptions are -tracked in the type system, there is no silent propagation of exceptions, and +behavior changes, which is a good thing. Finally, because exceptions are +tracked in the type system, and there is no silent propagation of exceptions, and all points where an exception may be thrown are readily apparent visually, this also means that we do not have to worry very much about "exception safety". + ## `try`..`catch` Like most other things in Rust, and unlike other languages that I know of, @@ -100,28 +89,18 @@ thrown, it is passed to the `catch` block, and the `try`..`catch` evaluates to the value of the `catch` block. As with `if`..`else` expressions, the types of 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 }` +a single `Err` type); all exceptions are always caught; and there may only be one `catch` block. This dramatically simplifies thinking about the behavior of exception-handling code. - For example: +There are two variations on this theme: - try { - foo()?.bar()? - } catch e { - let x = baz(e); - quux(x, e); - } + 1. `try { EXPR }` - Here the caught exception is bound to an irrefutable pattern immediately - following the `catch`. - This form is convenient when one does not wish to do case analysis on the - caught exception. + In this case the `try` block evaluates directly to a `Result` + 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. 2. `try { EXPR } catch { PAT => EXPR, PAT => EXPR, ... }` @@ -134,88 +113,10 @@ convenient in different circumstances. Blue(bex) => quux(bex) } - Here the `catch` is not immediately followed by a pattern; instead, its body + Here the `catch` performs a `match` on the caught exception directly, using any number of - refutable patterns. - This form is convenient when one *does* wish to do case analysis on the - caught exception. - -While it may appear to be extravagant to provide both forms, there is reason to -do so: either form on its own leads to unavoidable rightwards drift under some -circumstances. - -The first form leads to rightwards drift if one wishes to `match` on the caught -exception: - - try { - foo()?.bar()? - } catch e { - match e { - Red(rex) => baz(rex), - Blue(bex) => quux(bex) - } - } - -This `match e` is quite redundant and unfortunate. - -The second form leads to rightwards drift if one wishes to do more complex -multi-statement work with the caught exception: - - try { - foo()?.bar()? - } catch { - e => { - let x = baz(e); - quux(x, e); - } - } - -This single case arm is quite redundant and unfortunate. - -Therefore, neither form can be considered strictly superior to the other, and it -is preferable to simply provide both. - -Finally, it is also possible to write a `try` block *without* a `catch` block: - - 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 -`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 { ... } - -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` -and closure *types*, desugaring to a type parameter with a `Carrier` bound on -the parent item (e.g. a HOF), but this would be considerably more involved, and -it's not clear whether there is value in doing so.) + refutable patterns. This form is convenient for checking and handling the + caught exception directly. # Detailed design @@ -225,6 +126,7 @@ translation. We make use of an "early exit from any block" feature which doesn't currently exist in the language, generalizes the current `break` and `return` constructs, and is independently useful. + ## Early exit from any block The capability can be exposed either by generalizing `break` to take an optional @@ -233,14 +135,14 @@ value argument and break out of any block (not just loops), or by generalizing just the outermost block of the function. This feature is independently useful and I believe it should be added, but as it is only used here in this RFC as an explanatory device, and implementing the RFC does not require exposing it, I am -going to arbitrarily choose the `return` syntax for the following and won't +going to arbitrarily choose the `break` syntax for the following and won't discuss the question further. -So we are extending `return` with an optional lifetime argument: `return 'a -EXPR`. This is an expression of type `!` which causes an early return from the +So we are extending `break` with an optional value argument: `break 'a EXPR`. +This is an expression of type `!` which causes an early return from the enclosing block specified by `'a`, which then evaluates to the value `EXPR` (of course, the type of `EXPR` must unify with the type of the last expression in -that block). +that block). This works for any block, not only loops. A completely artificial example: @@ -248,7 +150,7 @@ A completely artificial example: let my_thing = if have_thing { get_thing() } else { - return 'a None + break 'a None }; println!("found thing: {}", my_thing); Some(my_thing) @@ -256,109 +158,9 @@ A completely artificial example: Here if we don't have a thing, we escape from the block early with `None`. -If no lifetime is specified, it defaults to returning from the whole function: -in other words, the current behavior. We can pretend there is a magical lifetime -`'fn` which refers to the outermost block of the current function, which is the -default. - -## The trait - -Here we specify the trait for types which can be used to "carry" either a normal -result or an exception. There are several different, completely equivalent ways -to formulate it, which differ only in the set of methods: for other -possibilities, see the appendix. - - #[lang(carrier)] - trait Carrier { - type Normal; - type Exception; - fn embed_normal(from: Normal) -> Self; - fn embed_exception(from: Exception) -> Self; - fn translate>(from: Self) -> Other; - } - -This trait basically just states that `Self` is isomorphic to -`Result` for some types `Normal` and `Exception`. For greater -clarity on how these methods work, see the section on `impl`s below. (For a -simpler formulation of the trait using `Result` directly, see the appendix.) - -The `translate` method says that it should be possible to translate to any -*other* `Carrier` type which has the same `Normal` and `Exception` types. This -can be used to inspect the value by translating to a concrete type such as -`Result` and then, for example, pattern matching on it. +If no value is specified, it defaults to `()`: in other words, the current behavior. +We can also imagine there is a magical lifetime `'fn` which refers to the lifetime of the whole function: in this case, `break 'fn` is equivalent to `return`. -Laws: - - 1. For all `x`, `translate(embed_normal(x): A): B ` = `embed_normal(x): B`. - 2. For all `x`, `translate(embed_exception(x): A): B ` = `embed_exception(x): B`. - 3. For all `carrier`, `translate(translate(carrier: A): B): A` = `carrier: A`. - -Here I've used explicit type ascription syntax to make it clear that e.g. the -types of `embed_` on the left and right hand sides are different. - -The first two laws say that embedding a result `x` into one carrier type and -then translating it to a second carrier type should be the same as embedding it -into the second type directly. - -The third law says that translating to a different carrier type and then -translating back should be the identity function. - - -## `impl`s of the trait - - impl Carrier for Result { - type Normal = T; - type Exception = E; - fn embed_normal(a: T) -> Result { Ok(a) } - fn embed_exception(e: E) -> Result { Err(e) } - fn translate>(result: Result) -> Other { - match result { - Ok(a) => Other::embed_normal(a), - Err(e) => Other::embed_exception(e) - } - } - } - -As we can see, `translate` can be implemented by deconstructing ourself and then -re-embedding the contained value into the other carrier type. - - impl Carrier for Option { - type Normal = T; - type Exception = (); - fn embed_normal(a: T) -> Option { Some(a) } - fn embed_exception(e: ()) -> Option { None } - fn translate>(option: Option) -> Other { - match option { - Some(a) => Other::embed_normal(a), - None => Other::embed_exception(()) - } - } - } - -Potentially also: - - impl Carrier for bool { - type Normal = (); - type Exception = (); - fn embed_normal(a: ()) -> bool { true } - fn embed_exception(e: ()) -> bool { false } - fn translate>(b: bool) -> Other { - match b { - true => Other::embed_normal(()), - false => Other::embed_exception(()) - } - } - } - -The laws should be sufficient to rule out any "icky" impls. For example, an impl -for `Vec` where an exception is represented as the empty vector, and a normal -result as a single-element vector: here the third law fails, because if the -`Vec` has more than one element *to begin with*, then it's not possible to -translate to a different carrier type and then back without losing information. - -The `bool` impl may be surprising, or not useful, but it *is* well-behaved: -`bool` is, after all, isomorphic to `Result<(), ()>`. This `impl` may be -included or not; I don't have a strong opinion about it. ## Definition of constructs @@ -372,34 +174,21 @@ constructs, and a "deep" one which is "fully expanded". Of course, these could be defined in many equivalent ways: the below definitions are merely one way. - * Construct: - - throw EXPR - - Shallow: - - return 'here Carrier::embed_exception(EXPR) - - Where `'here` refers to the innermost enclosing `try` block, or to `'fn` if - there is none. As with `return`, `EXPR` may be omitted and defaults to `()`. - * Construct: EXPR? Shallow: - match translate(EXPR) { + match EXPR { Ok(a) => a, - Err(e) => throw e + Err(e) => break 'here Err(e) } - Deep: + Where `'here` refers to the innermost enclosing `try` block, or to `'fn` if + there is none. - match translate(EXPR) { - Ok(a) => a, - Err(e) => return 'here Carrier::embed_exception(e) - } + The `?` operator has the same precedence as `.`. * Construct: @@ -410,45 +199,16 @@ are merely one way. Shallow: 'here: { - Carrier::embed_normal(foo()?.bar()) + Ok(foo()?.bar()) } Deep: 'here: { - Carrier::embed_normal(match translate(foo()) { - Ok(a) => a, - Err(e) => return 'here Carrier::embed_exception(e) - }.bar()) - } - - * Construct: - - try { - foo()?.bar() - } catch e { - baz(e) - } - - Shallow: - - match try { - foo()?.bar() - } { - Ok(a) => a, - Err(e) => baz(e) - } - - Deep: - - match 'here: { - Carrier::embed_normal(match translate(foo()) { + Ok(match foo() { Ok(a) => a, - Err(e) => return 'here Carrier::embed_exception(e) + Err(e) => break 'here Err(e) }.bar()) - } { - Ok(a) => a, - Err(e) => baz(e) } * Construct: @@ -460,12 +220,13 @@ are merely one way. B(b) => quux(b) } - Shallow: + Shallow: - try { + match (try { foo()?.bar() - } catch e { - match e { + }) { + Ok(a) => a, + Err(e) => match e { A(a) => baz(a), B(b) => quux(b) } @@ -474,9 +235,9 @@ are merely one way. Deep: match 'here: { - Carrier::embed_normal(match translate(foo()) { + Ok(match foo() { Ok(a) => a, - Err(e) => return 'here Carrier::embed_exception(e) + Err(e) => break 'here Err(e) }.bar()) } { Ok(a) => a, @@ -486,38 +247,6 @@ are merely one way. } } - * Construct: - - fn foo(A) -> B throws C { - CODE - } - - Shallow: - - fn foo>(A) -> Car { - try { - 'fn: { - CODE - } - } - } - - Deep: - - fn foo>(A) -> Car { - 'here: { - Carrier::embed_normal('fn: { - CODE - }) - } - } - - (Here our desugaring runs into a stumbling block, and we resort to a pun: the - *whole function* should be conceptually wrapped in a `try` block, and a - `return` inside `CODE` should be embedded as a successful result into the - carrier, rather than escaping from the `try` block itself. We suggest this by - putting the "magical lifetime" `'fn` *inside* the `try` block.) - The fully expanded translations get quite gnarly, but that is why it's good that you don't have to write them! @@ -528,78 +257,47 @@ of their definitions. a source-to-source translation in this manner, they need not necessarily be *implemented* this way.) + ## Laws -Without any attempt at completeness, and modulo `translate()` between different -carrier types, here are some things which should be true: +Without any attempt at completeness, here are some things which should be true: * `try { foo() } ` = `Ok(foo())` - * `try { throw e } ` = `Err(e)` + * `try { Err(e)? } ` = `Err(e)` * `try { foo()? } ` = `foo()` * `try { foo() } catch e { e }` = `foo()` - * `try { throw e } catch e { e }` = `e` + * `try { Err(e)? } catch e { e }` = `e` * `try { Ok(foo()?) } catch e { Err(e) }` = `foo()` -## Misc - - * Our current lint for unused results could be replaced by one which warns for - any unused result of a type which implements `Carrier`. - - * If there is ever ambiguity due to the carrier type being underdetermined - (experience should reveal whether this is a problem in practice), we could - resolve it by defaulting to `Result`. (This would presumably involve making - `Result` a lang item.) - - * Translating between different carrier types with the same `Normal` and - `Exception` types *should*, but may not necessarily *currently* be, a no-op - most of the time. - - We should make it so that: - - * repr(`Option`) = repr(`Result`) - * repr(`bool`) = repr(`Option<()>`) = repr(`Result<(), ()>`) - - If these hold, then `translate` between these types could in theory be - compiled down to just a `transmute`. (Whether LLVM is smart enough to do - this, I don't know.) - - * The `translate()` function smells to me like a natural transformation between - functors, but I'm not category theorist enough for it to be obvious. - # Drawbacks - * Adds new constructs to the language. + * Increases the syntactic surface area of the language. - * Some people have a philosophical objection to "there's more than one way to - do it". + * No expressivity is added, only convenience. Some object to "there's more than one way to do it" on principle. - * Relative to first-class checked exceptions, our implementation options are - constrained: while actual checked exceptions could be implemented in a - similar way to this proposal, they could also be implemented using unwinding, - should we choose to do so, and we do not realistically have that option here. + * If at some future point we were to add higher-kinded types and syntactic sugar + for monads, a la Haskell's `do` or Scala's `for`, their functionality may overlap and result in redundancy. + However, a number of challenges would have to be overcome for a generic monadic sugar to be able to + fully supplant these features: the integration of higher-kinded types into Rust's type system in the + first place, the shape of a `Monad` `trait` in a language with lifetimes and move semantics, + interaction between the monadic control flow and Rust's native control flow (the "ambient monad"), + automatic upcasting of exception types via `Into` (the exception (`Either`, `Result`) monad normally does not + do this, and it's not clear whether it can), and potentially others. # Alternatives - * Do nothing. + * Don't. - * Only add the `?` operator, but not any of the other constructs. + * Only add the `?` operator, but not `try`..`catch`. * Instead of a built-in `try`..`catch` construct, attempt to define one using macros. However, this is likely to be awkward because, at least, macros may only have their contents as a single block, rather than two. Furthermore, macros are excellent as a "safety net" for features which we forget to add - to the language itself, or which only have specialized use cases; but after - seeing this proposal, we need not forget `try`..`catch`, and its prevalence - in nearly every existing language suggests that it is, in fact, generally - useful. - - * Instead of a general `Carrier` trait, define everything directly in terms of - `Result`. This has precedent in that, for example, the `if`..`else` construct - is also defined directly in terms of `bool`. (However, this would likely also - lead to removing `Option` from the standard library in favor of - `Result<_, ()>`.) + to the language itself, or which only have specialized use cases; but generally + useful control flow constructs still work better as language features. * Add [first-class checked exceptions][notes], which are propagated automatically (without an `?` operator). @@ -615,27 +313,220 @@ carrier types, here are some things which should be true: [notes]: https://github.com/glaebhoerl/rust-notes/blob/268266e8fbbbfd91098d3bea784098e918b42322/my_rfcs/Exceptions.txt + * Wait (and hope) for HKTs and generic monad sugar. + + +# Future possibilities + +## An additional `catch` form to bind the caught exception irrefutably + +The `catch` described above immediately passes the caught exception into a `match` block. +It may sometimes be desirable to instead bind it directly to a single variable. That might +look like this: + + try { EXPR } catch IRR-PAT { EXPR } + +Where `catch` is followed by any irrefutable pattern (as with `let`). + +For example: + + try { + foo()?.bar()? + } catch e { + let x = baz(e); + quux(x, e); + } + +While it may appear to be extravagant to provide both forms, there is reason to +do so: either form on its own leads to unavoidable rightwards drift under some +circumstances. + +The first form leads to rightwards drift if one wishes to do more complex +multi-statement work with the caught exception: + + try { + foo()?.bar()? + } catch { + e => { + let x = baz(e); + quux(x, e); + } + } + +This single case arm is quite redundant and unfortunate. + +The second form leads to rightwards drift if one wishes to `match` on the caught +exception: + + try { + foo()?.bar()? + } catch e { + match e { + Red(rex) => baz(rex), + Blue(bex) => quux(bex) + } + } + +This `match e` is quite redundant and unfortunate. + +Therefore, neither form can be considered strictly superior to the other, and it +may be preferable to simply provide both. + + +## `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 { ... } + +would mean 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 turned into `Ok` or `Err` for the caller. 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. + + +## Generalize over `Result`, `Option`, and other result-carrying types + +`Option` is completely equivalent to `Result` modulo names, and many common APIs +use the `Option` type, so it would make sense to extend all of the above syntax to `Option`, +and other (potentially user-defined) equivalent-to-`Result` types, as well. + +This can be done by specifying a trait for types which can be used to "carry" either a normal +result or an exception. There are several different, equivalent ways +to formulate it, which differ in the set of methods provided, but the meaning in any case is essentially just +that you can choose some types `Normal` and `Exception` such that `Self` is isomorphic to `Result`. + +Here is one way: + + #[lang(result_carrier)] + trait ResultCarrier { + type Normal; + type Exception; + fn embed_normal(from: Normal) -> Self; + fn embed_exception(from: Exception) -> Self; + fn translate>(from: Self) -> Other; + } + +For greater clarity on how these methods work, see the section on `impl`s below. (For a +simpler formulation of the trait using `Result` directly, see further below.) + +The `translate` method says that it should be possible to translate to any +*other* `ResultCarrier` type which has the same `Normal` and `Exception` types. +This may not appear to be very useful, but in fact, this is what can be used to inspect the result, +by translating it to a concrete type such as `Result` and then, for example, pattern matching on it. + +Laws: + + 1. For all `x`, `translate(embed_normal(x): A): B ` = `embed_normal(x): B`. + 2. For all `x`, `translate(embed_exception(x): A): B ` = `embed_exception(x): B`. + 3. For all `carrier`, `translate(translate(carrier: A): B): A` = `carrier: A`. + +Here I've used explicit type ascription syntax to make it clear that e.g. the +types of `embed_` on the left and right hand sides are different. + +The first two laws say that embedding a result `x` into one result-carrying type and +then translating it to a second result-carrying type should be the same as embedding it +into the second type directly. + +The third law says that translating to a different result-carrying type and then +translating back should be a no-op. + + +## `impl`s of the trait + + impl ResultCarrier for Result { + type Normal = T; + type Exception = E; + fn embed_normal(a: T) -> Result { Ok(a) } + fn embed_exception(e: E) -> Result { Err(e) } + fn translate>(result: Result) -> Other { + match result { + Ok(a) => Other::embed_normal(a), + Err(e) => Other::embed_exception(e) + } + } + } + +As we can see, `translate` can be implemented by deconstructing ourself and then +re-embedding the contained value into the other result-carrying type. + + impl ResultCarrier for Option { + type Normal = T; + type Exception = (); + fn embed_normal(a: T) -> Option { Some(a) } + fn embed_exception(e: ()) -> Option { None } + fn translate>(option: Option) -> Other { + match option { + Some(a) => Other::embed_normal(a), + None => Other::embed_exception(()) + } + } + } + +Potentially also: + + impl ResultCarrier for bool { + type Normal = (); + type Exception = (); + fn embed_normal(a: ()) -> bool { true } + fn embed_exception(e: ()) -> bool { false } + fn translate>(b: bool) -> Other { + match b { + true => Other::embed_normal(()), + false => Other::embed_exception(()) + } + } + } + +The laws should be sufficient to rule out any "icky" impls. For example, an impl +for `Vec` where an exception is represented as the empty vector, and a normal +result as a single-element vector: here the third law fails, because if the +`Vec` has more than one element *to begin with*, then it's not possible to +translate to a different result-carrying type and then back without losing information. + +The `bool` impl may be surprising, or not useful, but it *is* well-behaved: +`bool` is, after all, isomorphic to `Result<(), ()>`. + +### Other miscellaneous notes about `ResultCarrier` -# Unresolved questions + * Our current lint for unused results could be replaced by one which warns for + any unused result of a type which implements `ResultCarrier`. - * What should the precedence of the `?` operator be? + * If there is ever ambiguity due to the result-carrying type being underdetermined + (experience should reveal whether this is a problem in practice), we could + resolve it by defaulting to `Result`. - * Should we add `throw` and/or `throws`? + * Translating between different result-carrying types with the same `Normal` and + `Exception` types *should*, but may not necessarily *currently* be, a + machine-level no-op most of the time. - * Should we have `impl Carrier for bool`? + We could/should make it so that: - * 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`?) + * repr(`Option`) = repr(`Result`) + * repr(`bool`) = repr(`Option<()>`) = repr(`Result<(), ()>`) + If these hold, then `translate` between these types could in theory be + compiled down to just a `transmute`. (Whether LLVM is smart enough to do + this, I don't know.) + + * The `translate()` function smells to me like a natural transformation between + functors, but I'm not category theorist enough for it to be obvious. -# Appendices -## Alternative formulations of the `Carrier` trait +### Alternative formulations of the `ResultCarrier` trait All of these have the form: - trait Carrier { + trait ResultCarrier { type Normal; type Exception; ...methods... @@ -643,7 +534,7 @@ All of these have the form: and differ only in the methods, which will be given. -### Explicit isomorphism with `Result` +#### Explicit isomorphism with `Result` fn from_result(Result) -> Self; fn to_result(Self) -> Result; @@ -653,7 +544,7 @@ This is, of course, the simplest possible formulation. The drawbacks are that it, in some sense, privileges `Result` over other potentially equivalent types, and that it may be less efficient for those types: for any non-`Result` type, every operation requires two method calls (one into -`Result`, and one out), whereas with the `Carrier` trait in the main text, they +`Result`, and one out), whereas with the `ResultCarrier` trait in the main text, they only require one. Laws: @@ -664,7 +555,7 @@ Laws: Laws for the remaining formulations below are left as an exercise for the reader. -### Avoid privileging `Result`, most naive version +#### Avoid privileging `Result`, most naive version fn embed_normal(Normal) -> Self; fn embed_exception(Exception) -> Self; @@ -675,7 +566,7 @@ reader. Of course this is horrible. -### Destructuring with HOFs (a.k.a. Church/Scott-encoding) +#### Destructuring with HOFs (a.k.a. Church/Scott-encoding) fn embed_normal(Normal) -> Self; fn embed_exception(Exception) -> Self; @@ -686,7 +577,7 @@ This is probably the right approach for Haskell, but not for Rust. With this formulation, because they each take ownership of them, the two closures may not even close over the same variables! -### Destructuring with HOFs, round 2 +#### Destructuring with HOFs, round 2 trait BiOnceFn { type ArgA; @@ -696,7 +587,7 @@ closures may not even close over the same variables! fn callB(Self, ArgB) -> Ret; } - trait Carrier { + trait ResultCarrier { type Normal; type Exception; fn normal(Normal) -> Self; From ef6bb5c4c4657746392f913157eaaab37edf3638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lehel?= Date: Tue, 29 Dec 2015 22:24:04 +0100 Subject: [PATCH 4/9] make clearer that early-exit-from-any-block is not proposed, mention it in "Future possibilities" --- active/0000-trait-based-exception-handling.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md index 202d6058593..74d1b46504b 100644 --- a/active/0000-trait-based-exception-handling.md +++ b/active/0000-trait-based-exception-handling.md @@ -132,9 +132,8 @@ constructs, and is independently useful. The capability can be exposed either by generalizing `break` to take an optional value argument and break out of any block (not just loops), or by generalizing `return` to take an optional lifetime argument and return from any block, not -just the outermost block of the function. This feature is independently useful -and I believe it should be added, but as it is only used here in this RFC as an -explanatory device, and implementing the RFC does not require exposing it, I am +just the outermost block of the function. This feature is only used in this RFC as an +explanatory device, and implementing the RFC does not require exposing it, so I am going to arbitrarily choose the `break` syntax for the following and won't discuss the question further. @@ -161,6 +160,8 @@ Here if we don't have a thing, we escape from the block early with `None`. If no value is specified, it defaults to `()`: in other words, the current behavior. We can also imagine there is a magical lifetime `'fn` which refers to the lifetime of the whole function: in this case, `break 'fn` is equivalent to `return`. +Again, this RFC does not propose generalizing `break` in this way at this time: it is only used as a way to explain the meaning of the constructs it does propose. + ## Definition of constructs @@ -318,6 +319,10 @@ Without any attempt at completeness, here are some things which should be true: # Future possibilities +## Expose a generalized form of `break` or `return` as described + +This RFC doesn't propose doing so at this time, but as it would be an independently useful feature, it could be added as well. + ## An additional `catch` form to bind the caught exception irrefutably The `catch` described above immediately passes the caught exception into a `match` block. From 4288a756fd2ce132ba6330a03db277dfd74123be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lehel?= Date: Wed, 30 Dec 2015 03:30:08 +0100 Subject: [PATCH 5/9] add syntax as an unresolved question --- active/0000-trait-based-exception-handling.md | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md index 74d1b46504b..29e787ddb4f 100644 --- a/active/0000-trait-based-exception-handling.md +++ b/active/0000-trait-based-exception-handling.md @@ -146,7 +146,7 @@ that block). This works for any block, not only loops. A completely artificial example: 'a: { - let my_thing = if have_thing { + let my_thing = if have_thing() { get_thing() } else { break 'a None @@ -271,6 +271,37 @@ Without any attempt at completeness, here are some things which should be true: * `try { Ok(foo()?) } catch e { Err(e) }` = `foo()` +# Unresolved questions + +## Choice of keywords + +The RFC to this point uses the keywords `try`..`catch`, but there are a number of other possibilities, each with different advantages and drawbacks: + + * `try { ... } catch { ... }` + + * `try { ... } match { ... }` + + * `try { ... } handle { ... }` + + * `catch { ... } match { ... }` + + * `catch { ... } handle { ... }` + + * `catch ...` (without braces or a second clause) + +Among the considerations: + + * Simplicity. Brevity. + + * Following precedent from existing, popular languages, and familiarity with respect to analogous constructs in them. + + * Fidelity to the constructs' actual behavior. For instance, the first clause always catches the "exception"; the second only branches on it. + + * Consistency with the existing `try!()` macro. If the first clause is called `try`, then `try { }` and `try!()` would have essentially inverse meanings. + + * Language-level backwards compatibility when adding new keywords. I'm not sure how this could or should be handled. + + # Drawbacks * Increases the syntactic surface area of the language. @@ -291,7 +322,9 @@ Without any attempt at completeness, here are some things which should be true: * Don't. - * Only add the `?` operator, but not `try`..`catch`. + * Only add the `?` operator, but not `try` and `try`..`catch`. + + * Only add `?` and `try`, but not `try`..`catch`. * Instead of a built-in `try`..`catch` construct, attempt to define one using macros. However, this is likely to be awkward because, at least, macros may @@ -312,10 +345,10 @@ Without any attempt at completeness, here are some things which should be true: serious an issue this would actually be in practice, I don't know - there's reason to believe that it would be much less of one than in C++. -[notes]: https://github.com/glaebhoerl/rust-notes/blob/268266e8fbbbfd91098d3bea784098e918b42322/my_rfcs/Exceptions.txt - * Wait (and hope) for HKTs and generic monad sugar. +[notes]: https://github.com/glaebhoerl/rust-notes/blob/268266e8fbbbfd91098d3bea784098e918b42322/my_rfcs/Exceptions.txt + # Future possibilities @@ -377,7 +410,6 @@ This `match e` is quite redundant and unfortunate. Therefore, neither form can be considered strictly superior to the other, and it may be preferable to simply provide both. - ## `throw` and `throws` It is possible to carry the exception handling analogy further and also add @@ -398,11 +430,10 @@ both "normal" and "throwing" code paths and (apart from `?` to propagate exceptions) matches what code might look like in a language with native exceptions. - ## Generalize over `Result`, `Option`, and other result-carrying types `Option` is completely equivalent to `Result` modulo names, and many common APIs -use the `Option` type, so it would make sense to extend all of the above syntax to `Option`, +use the `Option` type, so it would be useful to extend all of the above syntax to `Option`, and other (potentially user-defined) equivalent-to-`Result` types, as well. This can be done by specifying a trait for types which can be used to "carry" either a normal From f5bf33127b1a1e3dfbb5016aa1284d0e81ea0f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lehel?= Date: Wed, 30 Dec 2015 04:25:41 +0100 Subject: [PATCH 6/9] Add exception-upcasting with `Into` and minor other stuff --- active/0000-trait-based-exception-handling.md | 72 ++++++++++++++++--- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md index 29e787ddb4f..abc2146cda7 100644 --- a/active/0000-trait-based-exception-handling.md +++ b/active/0000-trait-based-exception-handling.md @@ -53,8 +53,8 @@ chaining method calls which may each "throw an exception": foo()?.bar()?.baz() -(Naturally, in this case the types of the "exceptions thrown by" `foo()` and -`bar()` must unify.) +Naturally, in this case the types of the "exceptions thrown by" `foo()` and +`bar()` must unify. Like the current `try!()` macro, the `?` operator will also perform an implicit "upcast" on the exception type. When used outside of a `try` block, the `?` operator propagates the exception to the caller of the current function, just like the current `try!` macro does. (If @@ -79,6 +79,23 @@ tracked in the type system, and there is no silent propagation of exceptions, an all points where an exception may be thrown are readily apparent visually, this also means that we do not have to worry very much about "exception safety". +### Exception type upcasting + +In a language with checked exceptions and subtyping, it is clear that if a function is declared as throwing a particular type, its body should also be able to throw any of its subtypes. Similarly, in a language with structural sum types (a.k.a. anonymous `enum`s, polymorphic variants), one should be able to throw a type with fewer cases in a function declaring that it may throw a superset of those cases. This is essentially what is achieved by the common Rust practice of declaring a custom error `enum` with `From` `impl`s for each of the upstream error types which may be propagated: + + enum MyError { + IoError(io::Error), + JsonError(json::Error), + OtherError(...) + } + + impl From for MyError { ... } + impl From for MyError { ... } + +Here `io::Error` and `json::Error` can be thought of as subtypes of `MyError`, with a clear and direct embedding into the supertype. + +The `?` operator should therefore perform such an implicit conversion in the nature of a subtype-to-supertype coercion. The present RFC uses the `std::convert::Into` trait for this purpose (which has a blanket `impl` forwarding from `From`). The precise requirements for a conversion to be "like" a subtyping coercion are an open question; see the "Unresolved questions" section. + ## `try`..`catch` @@ -183,7 +200,7 @@ are merely one way. match EXPR { Ok(a) => a, - Err(e) => break 'here Err(e) + Err(e) => break 'here Err(e.into()) } Where `'here` refers to the innermost enclosing `try` block, or to `'fn` if @@ -208,7 +225,7 @@ are merely one way. 'here: { Ok(match foo() { Ok(a) => a, - Err(e) => break 'here Err(e) + Err(e) => break 'here Err(e.into()) }.bar()) } @@ -238,7 +255,7 @@ are merely one way. match 'here: { Ok(match foo() { Ok(a) => a, - Err(e) => break 'here Err(e) + Err(e) => break 'here Err(e.into()) }.bar()) } { Ok(a) => a, @@ -263,16 +280,19 @@ a source-to-source translation in this manner, they need not necessarily be Without any attempt at completeness, here are some things which should be true: - * `try { foo() } ` = `Ok(foo())` - * `try { Err(e)? } ` = `Err(e)` - * `try { foo()? } ` = `foo()` - * `try { foo() } catch e { e }` = `foo()` - * `try { Err(e)? } catch e { e }` = `e` - * `try { Ok(foo()?) } catch e { Err(e) }` = `foo()` + * `try { foo() } ` = `Ok(foo())` + * `try { Err(e)? } ` = `Err(e.into())` + * `try { try_foo()? } ` = `try_foo().map_err(Into::into)` + * `try { Err(e)? } catch { e => e }` = `e.into()` + * `try { Ok(try_foo()?) } catch { e => Err(e) }` = `try_foo().map_err(Into::into)` + +(In the above, `foo()` is a function returning any type, and `try_foo()` is a function returning a `Result`.) # Unresolved questions +These questions should be satisfactorally resolved before stabilizing the relevant features, at the latest. + ## Choice of keywords The RFC to this point uses the keywords `try`..`catch`, but there are a number of other possibilities, each with different advantages and drawbacks: @@ -302,6 +322,36 @@ Among the considerations: * Language-level backwards compatibility when adding new keywords. I'm not sure how this could or should be handled. +## Semantics for "upcasting" + +What should the contract for a `From`/`Into` `impl` be? Are these even the right `trait`s to use for this feature? + +Two obvious, minimal requirements are: + + * It should be pure: no side effects, and no observation of side effects. (The result should depend *only* on the argument.) + + * It should be total: no panics or other divergence, except perhaps in the case of resource exhaustion (OOM, stack overflow). + +The other requirements for an implicit conversion to be well-behaved in the context of this feature should be thought through with care. + +Some further thoughts and possibilities on this matter: + + * It should be "like a coercion from subtype to supertype", as described earlier. The precise meaning of this is not obvious. + + * A common condition on subtyping coercions is coherence: if you can compound-coerce to go from `A` to `Z` indirectly along multiple different paths, they should all have the same end result. + + * It should be unambiguous, or preserve the meaning of the input: `impl From for u32` as `x as u32` feels right; as `(x as u32) * 12345` feels wrong, even though this is perfectly pure, total, and injective. What this means precisely in the general case is unclear. + + * It should be lossless, or in other words, injective: it should map each observably-different element of the input type to observably-different elements of the output type. (Observably-different means that it is possible to write a program which behaves differently depending on which one it gets, modulo things that "shouldn't count" like observing execution time or resource usage.) + + * The types converted between should the "same kind of thing": for instance, the *existing* `impl From for Ipv4Addr` is pretty suspect on this count. (This perhaps ties into the subtyping angle: `Ipv4Addr` is clearly not a supertype of `u32`.) + + +## Forwards-compatibility + +If we later want to generalize this feature to other types such as `Option`, as described below, will we be able to do so while maintaining backwards-compatibility? + + # Drawbacks * Increases the syntactic surface area of the language. From 1a50c01eb72d4ca49880f1d9a8617f72379166e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lehel?= Date: Wed, 30 Dec 2015 04:39:41 +0100 Subject: [PATCH 7/9] wrap ALL the words; also mention lang items --- active/0000-trait-based-exception-handling.md | 280 +++++++++++------- 1 file changed, 170 insertions(+), 110 deletions(-) diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md index abc2146cda7..6c5d4c04f3d 100644 --- a/active/0000-trait-based-exception-handling.md +++ b/active/0000-trait-based-exception-handling.md @@ -5,33 +5,35 @@ # Summary -Add syntactic sugar for working with the `Result` type which models common exception handling constructs. +Add syntactic sugar for working with the `Result` type which models common +exception handling constructs. The new constructs are: * An `?` operator for explicitly propagating "exceptions". - * A `try`..`catch` construct for conveniently catching and handling "exceptions". + * A `try`..`catch` construct for conveniently catching and handling + "exceptions". -The idea for the `?` operator originates from [RFC PR 204][204] by [@aturon](https://github.com/aturon). +The idea for the `?` operator originates from [RFC PR 204][204] by +[@aturon](https://github.com/aturon). [204]: https://github.com/rust-lang/rfcs/pull/204 # Motivation and overview -Rust currently uses the `enum Result` type for error -handling. This solution is simple, well-behaved, and easy to understand, but -often gnarly and inconvenient to work with. We would like to solve the latter -problem while retaining the other nice properties and avoiding duplication of -functionality. +Rust currently uses the `enum Result` type for error handling. This solution is +simple, well-behaved, and easy to understand, but often gnarly and inconvenient +to work with. We would like to solve the latter problem while retaining the +other nice properties and avoiding duplication of functionality. We can accomplish this by adding constructs which mimic the exception-handling constructs of other languages in both appearance and behavior, while improving -upon them in typically Rustic fashion. Their meaning can be specified by a straightforward -source-to-source translation into existing language constructs, plus a very -simple and obvious new one. (They may also, but need not necessarily, be -implemented in this way.) +upon them in typically Rustic fashion. Their meaning can be specified by a +straightforward source-to-source translation into existing language constructs, +plus a very simple and obvious new one. (They may also, but need not +necessarily, be implemented in this way.) These constructs are strict additions to the existing language, and apart from the issue of keywords, the legality and behavior of all currently existing Rust @@ -39,49 +41,58 @@ programs is entirely unaffected. The most important additions are a postfix `?` operator for propagating "exceptions" and a `try`..`catch` block for catching and handling them. By an -"exception", we essentially just mean the `Err` variant of a `Result`. (See the "Detailed design" section for more -precision.) +"exception", we essentially just mean the `Err` variant of a `Result`. (See the +"Detailed design" section for more precision.) ## `?` operator -The postfix `?` operator can be applied to `Result` values and is equivalent to the current `try!()` macro. It either -returns the `Ok` value directly, or performs an early exit and propagates -the `Err` value further out. (So given `my_result: Result`, we -have `my_result?: Foo`.) This allows it to be used for e.g. conveniently -chaining method calls which may each "throw an exception": +The postfix `?` operator can be applied to `Result` values and is equivalent to +the current `try!()` macro. It either returns the `Ok` value directly, or +performs an early exit and propagates the `Err` value further out. (So given +`my_result: Result`, we have `my_result?: Foo`.) This allows it to be +used for e.g. conveniently chaining method calls which may each "throw an +exception": foo()?.bar()?.baz() Naturally, in this case the types of the "exceptions thrown by" `foo()` and -`bar()` must unify. Like the current `try!()` macro, the `?` operator will also perform an implicit "upcast" on the exception type. +`bar()` must unify. Like the current `try!()` macro, the `?` operator will also +perform an implicit "upcast" on the exception type. When used outside of a `try` block, the `?` operator propagates the exception to the caller of the current function, just like the current `try!` macro does. (If -the return type of the function isn't a `Result`, then this is a type error.) When used inside a `try` -block, it propagates the exception up to the innermost `try` block, as one would -expect. +the return type of the function isn't a `Result`, then this is a type error.) +When used inside a `try` block, it propagates the exception up to the innermost +`try` block, as one would expect. Requiring an explicit `?` operator to propagate exceptions strikes a very pleasing balance between completely automatic exception propagation, which most -languages have, and completely manual propagation, which we'd have apart from the `try!` macro. It means that function calls -remain simply function calls which return a result to their caller, with no -magic going on behind the scenes; and this also *increases* flexibility, because -one gets to choose between propagation with `?` or consuming the returned -`Result` directly. +languages have, and completely manual propagation, which we'd have apart from +the `try!` macro. It means that function calls remain simply function calls +which return a result to their caller, with no magic going on behind the scenes; +and this also *increases* flexibility, because one gets to choose between +propagation with `?` or consuming the returned `Result` directly. The `?` operator itself is suggestive, syntactically lightweight enough to not be bothersome, and lets the reader determine at a glance where an exception may or may not be thrown. It also means that if the signature of a function changes with respect to exceptions, it will lead to type errors rather than silent -behavior changes, which is a good thing. Finally, because exceptions are -tracked in the type system, and there is no silent propagation of exceptions, and -all points where an exception may be thrown are readily apparent visually, this -also means that we do not have to worry very much about "exception safety". +behavior changes, which is a good thing. Finally, because exceptions are tracked +in the type system, and there is no silent propagation of exceptions, and all +points where an exception may be thrown are readily apparent visually, this also +means that we do not have to worry very much about "exception safety". ### Exception type upcasting -In a language with checked exceptions and subtyping, it is clear that if a function is declared as throwing a particular type, its body should also be able to throw any of its subtypes. Similarly, in a language with structural sum types (a.k.a. anonymous `enum`s, polymorphic variants), one should be able to throw a type with fewer cases in a function declaring that it may throw a superset of those cases. This is essentially what is achieved by the common Rust practice of declaring a custom error `enum` with `From` `impl`s for each of the upstream error types which may be propagated: +In a language with checked exceptions and subtyping, it is clear that if a +function is declared as throwing a particular type, its body should also be able +to throw any of its subtypes. Similarly, in a language with structural sum types +(a.k.a. anonymous `enum`s, polymorphic variants), one should be able to throw a +type with fewer cases in a function declaring that it may throw a superset of +those cases. This is essentially what is achieved by the common Rust practice of +declaring a custom error `enum` with `From` `impl`s for each of the upstream +error types which may be propagated: enum MyError { IoError(io::Error), @@ -92,9 +103,15 @@ In a language with checked exceptions and subtyping, it is clear that if a funct impl From for MyError { ... } impl From for MyError { ... } -Here `io::Error` and `json::Error` can be thought of as subtypes of `MyError`, with a clear and direct embedding into the supertype. +Here `io::Error` and `json::Error` can be thought of as subtypes of `MyError`, +with a clear and direct embedding into the supertype. -The `?` operator should therefore perform such an implicit conversion in the nature of a subtype-to-supertype coercion. The present RFC uses the `std::convert::Into` trait for this purpose (which has a blanket `impl` forwarding from `From`). The precise requirements for a conversion to be "like" a subtyping coercion are an open question; see the "Unresolved questions" section. +The `?` operator should therefore perform such an implicit conversion in the +nature of a subtype-to-supertype coercion. The present RFC uses the +`std::convert::Into` trait for this purpose (which has a blanket `impl` +forwarding from `From`). The precise requirements for a conversion to be "like" +a subtyping coercion are an open question; see the "Unresolved questions" +section. ## `try`..`catch` @@ -106,16 +123,18 @@ thrown, it is passed to the `catch` block, and the `try`..`catch` evaluates to the value of the `catch` block. As with `if`..`else` expressions, the types of 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); all exceptions are always caught; and there may only be one `catch` block. This dramatically simplifies thinking about the behavior of exception-handling code. +a single `Err` type); all exceptions are always caught; and there may only be +one `catch` block. This dramatically simplifies thinking about the behavior of +exception-handling code. There are two variations on this theme: 1. `try { EXPR }` - In this case the `try` block evaluates directly to a `Result` - 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 - + In this case the `try` block evaluates directly to a `Result` 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. @@ -130,10 +149,9 @@ There are two variations on this theme: Blue(bex) => quux(bex) } - Here the `catch` - performs a `match` on the caught exception directly, using any number of - refutable patterns. This form is convenient for checking and handling the - caught exception directly. + Here the `catch` performs a `match` on the caught exception directly, using + any number of refutable patterns. This form is convenient for checking and + handling the caught exception directly. # Detailed design @@ -149,10 +167,10 @@ constructs, and is independently useful. The capability can be exposed either by generalizing `break` to take an optional value argument and break out of any block (not just loops), or by generalizing `return` to take an optional lifetime argument and return from any block, not -just the outermost block of the function. This feature is only used in this RFC as an -explanatory device, and implementing the RFC does not require exposing it, so I am -going to arbitrarily choose the `break` syntax for the following and won't -discuss the question further. +just the outermost block of the function. This feature is only used in this RFC +as an explanatory device, and implementing the RFC does not require exposing it, +so I am going to arbitrarily choose the `break` syntax for the following and +won't discuss the question further. So we are extending `break` with an optional value argument: `break 'a EXPR`. This is an expression of type `!` which causes an early return from the @@ -174,10 +192,14 @@ A completely artificial example: Here if we don't have a thing, we escape from the block early with `None`. -If no value is specified, it defaults to `()`: in other words, the current behavior. -We can also imagine there is a magical lifetime `'fn` which refers to the lifetime of the whole function: in this case, `break 'fn` is equivalent to `return`. +If no value is specified, it defaults to `()`: in other words, the current +behavior. We can also imagine there is a magical lifetime `'fn` which refers to +the lifetime of the whole function: in this case, `break 'fn` is equivalent to +`return`. -Again, this RFC does not propose generalizing `break` in this way at this time: it is only used as a way to explain the meaning of the constructs it does propose. +Again, this RFC does not propose generalizing `break` in this way at this time: +it is only used as a way to explain the meaning of the constructs it does +propose. ## Definition of constructs @@ -275,6 +297,9 @@ of their definitions. a source-to-source translation in this manner, they need not necessarily be *implemented* this way.) +As a result of this RFC, both `Into` and `Result` would have to become lang +items. + ## Laws @@ -286,16 +311,19 @@ Without any attempt at completeness, here are some things which should be true: * `try { Err(e)? } catch { e => e }` = `e.into()` * `try { Ok(try_foo()?) } catch { e => Err(e) }` = `try_foo().map_err(Into::into)` -(In the above, `foo()` is a function returning any type, and `try_foo()` is a function returning a `Result`.) +(In the above, `foo()` is a function returning any type, and `try_foo()` is a +function returning a `Result`.) # Unresolved questions -These questions should be satisfactorally resolved before stabilizing the relevant features, at the latest. +These questions should be satisfactorally resolved before stabilizing the +relevant features, at the latest. ## Choice of keywords -The RFC to this point uses the keywords `try`..`catch`, but there are a number of other possibilities, each with different advantages and drawbacks: +The RFC to this point uses the keywords `try`..`catch`, but there are a number +of other possibilities, each with different advantages and drawbacks: * `try { ... } catch { ... }` @@ -313,59 +341,85 @@ Among the considerations: * Simplicity. Brevity. - * Following precedent from existing, popular languages, and familiarity with respect to analogous constructs in them. + * Following precedent from existing, popular languages, and familiarity with + respect to analogous constructs in them. - * Fidelity to the constructs' actual behavior. For instance, the first clause always catches the "exception"; the second only branches on it. + * Fidelity to the constructs' actual behavior. For instance, the first clause + always catches the "exception"; the second only branches on it. - * Consistency with the existing `try!()` macro. If the first clause is called `try`, then `try { }` and `try!()` would have essentially inverse meanings. + * Consistency with the existing `try!()` macro. If the first clause is called + `try`, then `try { }` and `try!()` would have essentially inverse meanings. - * Language-level backwards compatibility when adding new keywords. I'm not sure how this could or should be handled. + * Language-level backwards compatibility when adding new keywords. I'm not sure + how this could or should be handled. ## Semantics for "upcasting" -What should the contract for a `From`/`Into` `impl` be? Are these even the right `trait`s to use for this feature? +What should the contract for a `From`/`Into` `impl` be? Are these even the right +`trait`s to use for this feature? Two obvious, minimal requirements are: - * It should be pure: no side effects, and no observation of side effects. (The result should depend *only* on the argument.) + * It should be pure: no side effects, and no observation of side effects. (The + result should depend *only* on the argument.) - * It should be total: no panics or other divergence, except perhaps in the case of resource exhaustion (OOM, stack overflow). + * It should be total: no panics or other divergence, except perhaps in the case + of resource exhaustion (OOM, stack overflow). -The other requirements for an implicit conversion to be well-behaved in the context of this feature should be thought through with care. +The other requirements for an implicit conversion to be well-behaved in the +context of this feature should be thought through with care. Some further thoughts and possibilities on this matter: - * It should be "like a coercion from subtype to supertype", as described earlier. The precise meaning of this is not obvious. + * It should be "like a coercion from subtype to supertype", as described + earlier. The precise meaning of this is not obvious. - * A common condition on subtyping coercions is coherence: if you can compound-coerce to go from `A` to `Z` indirectly along multiple different paths, they should all have the same end result. + * A common condition on subtyping coercions is coherence: if you can + compound-coerce to go from `A` to `Z` indirectly along multiple different + paths, they should all have the same end result. - * It should be unambiguous, or preserve the meaning of the input: `impl From for u32` as `x as u32` feels right; as `(x as u32) * 12345` feels wrong, even though this is perfectly pure, total, and injective. What this means precisely in the general case is unclear. + * It should be unambiguous, or preserve the meaning of the input: + `impl From for u32` as `x as u32` feels right; as `(x as u32) * 12345` + feels wrong, even though this is perfectly pure, total, and injective. What + this means precisely in the general case is unclear. - * It should be lossless, or in other words, injective: it should map each observably-different element of the input type to observably-different elements of the output type. (Observably-different means that it is possible to write a program which behaves differently depending on which one it gets, modulo things that "shouldn't count" like observing execution time or resource usage.) + * It should be lossless, or in other words, injective: it should map each + observably-different element of the input type to observably-different + elements of the output type. (Observably-different means that it is possible + to write a program which behaves differently depending on which one it gets, + modulo things that "shouldn't count" like observing execution time or + resource usage.) - * The types converted between should the "same kind of thing": for instance, the *existing* `impl From for Ipv4Addr` is pretty suspect on this count. (This perhaps ties into the subtyping angle: `Ipv4Addr` is clearly not a supertype of `u32`.) + * The types converted between should the "same kind of thing": for instance, + the *existing* `impl From for Ipv4Addr` is pretty suspect on this count. + (This perhaps ties into the subtyping angle: `Ipv4Addr` is clearly not a + supertype of `u32`.) ## Forwards-compatibility -If we later want to generalize this feature to other types such as `Option`, as described below, will we be able to do so while maintaining backwards-compatibility? +If we later want to generalize this feature to other types such as `Option`, as +described below, will we be able to do so while maintaining backwards-compatibility? # Drawbacks * Increases the syntactic surface area of the language. - * No expressivity is added, only convenience. Some object to "there's more than one way to do it" on principle. + * No expressivity is added, only convenience. Some object to "there's more than + one way to do it" on principle. - * If at some future point we were to add higher-kinded types and syntactic sugar - for monads, a la Haskell's `do` or Scala's `for`, their functionality may overlap and result in redundancy. - However, a number of challenges would have to be overcome for a generic monadic sugar to be able to - fully supplant these features: the integration of higher-kinded types into Rust's type system in the - first place, the shape of a `Monad` `trait` in a language with lifetimes and move semantics, - interaction between the monadic control flow and Rust's native control flow (the "ambient monad"), - automatic upcasting of exception types via `Into` (the exception (`Either`, `Result`) monad normally does not - do this, and it's not clear whether it can), and potentially others. + * If at some future point we were to add higher-kinded types and syntactic + sugar for monads, a la Haskell's `do` or Scala's `for`, their functionality + may overlap and result in redundancy. However, a number of challenges would + have to be overcome for a generic monadic sugar to be able to fully supplant + these features: the integration of higher-kinded types into Rust's type + system in the first place, the shape of a `Monad` `trait` in a language with + lifetimes and move semantics, interaction between the monadic control flow + and Rust's native control flow (the "ambient monad"), automatic upcasting of + exception types via `Into` (the exception (`Either`, `Result`) monad normally + does not do this, and it's not clear whether it can), and potentially others. # Alternatives @@ -380,8 +434,9 @@ If we later want to generalize this feature to other types such as `Option`, as macros. However, this is likely to be awkward because, at least, macros may only have their contents as a single block, rather than two. Furthermore, macros are excellent as a "safety net" for features which we forget to add - to the language itself, or which only have specialized use cases; but generally - useful control flow constructs still work better as language features. + to the language itself, or which only have specialized use cases; but + generally useful control flow constructs still work better as language + features. * Add [first-class checked exceptions][notes], which are propagated automatically (without an `?` operator). @@ -408,9 +463,9 @@ This RFC doesn't propose doing so at this time, but as it would be an independen ## An additional `catch` form to bind the caught exception irrefutably -The `catch` described above immediately passes the caught exception into a `match` block. -It may sometimes be desirable to instead bind it directly to a single variable. That might -look like this: +The `catch` described above immediately passes the caught exception into a +`match` block. It may sometimes be desirable to instead bind it directly to a +single variable. That might look like this: try { EXPR } catch IRR-PAT { EXPR } @@ -473,23 +528,25 @@ A `throws` clause on a function: fn foo(arg: Foo) -> Bar throws Baz { ... } -would mean 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 turned into `Ok` or `Err` for the caller. 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. +would mean 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 turned into `Ok` or `Err` for the caller. 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. ## Generalize over `Result`, `Option`, and other result-carrying types -`Option` is completely equivalent to `Result` modulo names, and many common APIs -use the `Option` type, so it would be useful to extend all of the above syntax to `Option`, -and other (potentially user-defined) equivalent-to-`Result` types, as well. +`Option` is completely equivalent to `Result` modulo names, and many +common APIs use the `Option` type, so it would be useful to extend all of the +above syntax to `Option`, and other (potentially user-defined) +equivalent-to-`Result` types, as well. -This can be done by specifying a trait for types which can be used to "carry" either a normal -result or an exception. There are several different, equivalent ways -to formulate it, which differ in the set of methods provided, but the meaning in any case is essentially just -that you can choose some types `Normal` and `Exception` such that `Self` is isomorphic to `Result`. +This can be done by specifying a trait for types which can be used to "carry" +either a normal result or an exception. There are several different, equivalent +ways to formulate it, which differ in the set of methods provided, but the +meaning in any case is essentially just that you can choose some types `Normal` +and `Exception` such that `Self` is isomorphic to `Result`. Here is one way: @@ -502,13 +559,15 @@ Here is one way: fn translate>(from: Self) -> Other; } -For greater clarity on how these methods work, see the section on `impl`s below. (For a -simpler formulation of the trait using `Result` directly, see further below.) +For greater clarity on how these methods work, see the section on `impl`s below. +(For a simpler formulation of the trait using `Result` directly, see further +below.) The `translate` method says that it should be possible to translate to any *other* `ResultCarrier` type which has the same `Normal` and `Exception` types. -This may not appear to be very useful, but in fact, this is what can be used to inspect the result, -by translating it to a concrete type such as `Result` and then, for example, pattern matching on it. +This may not appear to be very useful, but in fact, this is what can be used to +inspect the result, by translating it to a concrete type such as `Result` and then, for example, pattern matching on it. Laws: @@ -519,9 +578,9 @@ Laws: Here I've used explicit type ascription syntax to make it clear that e.g. the types of `embed_` on the left and right hand sides are different. -The first two laws say that embedding a result `x` into one result-carrying type and -then translating it to a second result-carrying type should be the same as embedding it -into the second type directly. +The first two laws say that embedding a result `x` into one result-carrying type +and then translating it to a second result-carrying type should be the same as +embedding it into the second type directly. The third law says that translating to a different result-carrying type and then translating back should be a no-op. @@ -577,7 +636,8 @@ The laws should be sufficient to rule out any "icky" impls. For example, an impl for `Vec` where an exception is represented as the empty vector, and a normal result as a single-element vector: here the third law fails, because if the `Vec` has more than one element *to begin with*, then it's not possible to -translate to a different result-carrying type and then back without losing information. +translate to a different result-carrying type and then back without losing +information. The `bool` impl may be surprising, or not useful, but it *is* well-behaved: `bool` is, after all, isomorphic to `Result<(), ()>`. @@ -587,12 +647,12 @@ The `bool` impl may be surprising, or not useful, but it *is* well-behaved: * Our current lint for unused results could be replaced by one which warns for any unused result of a type which implements `ResultCarrier`. - * If there is ever ambiguity due to the result-carrying type being underdetermined - (experience should reveal whether this is a problem in practice), we could - resolve it by defaulting to `Result`. + * If there is ever ambiguity due to the result-carrying type being + underdetermined (experience should reveal whether this is a problem in + practice), we could resolve it by defaulting to `Result`. - * Translating between different result-carrying types with the same `Normal` and - `Exception` types *should*, but may not necessarily *currently* be, a + * Translating between different result-carrying types with the same `Normal` + and `Exception` types *should*, but may not necessarily *currently* be, a machine-level no-op most of the time. We could/should make it so that: @@ -630,8 +690,8 @@ This is, of course, the simplest possible formulation. The drawbacks are that it, in some sense, privileges `Result` over other potentially equivalent types, and that it may be less efficient for those types: for any non-`Result` type, every operation requires two method calls (one into -`Result`, and one out), whereas with the `ResultCarrier` trait in the main text, they -only require one. +`Result`, and one out), whereas with the `ResultCarrier` trait in the main text, +they only require one. Laws: From a12bad60bd9e66420b9debaf82d7513dcbd9db4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lehel?= Date: Wed, 30 Dec 2015 20:54:37 +0100 Subject: [PATCH 8/9] minor verbiage --- active/0000-trait-based-exception-handling.md | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md index 6c5d4c04f3d..0d868aaaee4 100644 --- a/active/0000-trait-based-exception-handling.md +++ b/active/0000-trait-based-exception-handling.md @@ -41,8 +41,7 @@ programs is entirely unaffected. The most important additions are a postfix `?` operator for propagating "exceptions" and a `try`..`catch` block for catching and handling them. By an -"exception", we essentially just mean the `Err` variant of a `Result`. (See the -"Detailed design" section for more precision.) +"exception", for now, we essentially just mean the `Err` variant of a `Result`. ## `?` operator @@ -106,7 +105,7 @@ error types which may be propagated: Here `io::Error` and `json::Error` can be thought of as subtypes of `MyError`, with a clear and direct embedding into the supertype. -The `?` operator should therefore perform such an implicit conversion in the +The `?` operator should therefore perform such an implicit conversion, in the nature of a subtype-to-supertype coercion. The present RFC uses the `std::convert::Into` trait for this purpose (which has a blanket `impl` forwarding from `From`). The precise requirements for a conversion to be "like" @@ -150,8 +149,8 @@ There are two variations on this theme: } Here the `catch` performs a `match` on the caught exception directly, using - any number of refutable patterns. This form is convenient for checking and - handling the caught exception directly. + any number of refutable patterns. This form is convenient for handling the + exception in-place. # Detailed design @@ -274,12 +273,12 @@ are merely one way. Deep: - match 'here: { + match ('here: { Ok(match foo() { Ok(a) => a, Err(e) => break 'here Err(e.into()) }.bar()) - } { + }) { Ok(a) => a, Err(e) => match e { A(a) => baz(a), @@ -342,7 +341,7 @@ Among the considerations: * Simplicity. Brevity. * Following precedent from existing, popular languages, and familiarity with - respect to analogous constructs in them. + respect to their analogous constructs. * Fidelity to the constructs' actual behavior. For instance, the first clause always catches the "exception"; the second only branches on it. @@ -370,7 +369,7 @@ Two obvious, minimal requirements are: The other requirements for an implicit conversion to be well-behaved in the context of this feature should be thought through with care. -Some further thoughts and possibilities on this matter: +Some further thoughts and possibilities on this matter, only as brainstorming: * It should be "like a coercion from subtype to supertype", as described earlier. The precise meaning of this is not obvious. @@ -379,11 +378,6 @@ Some further thoughts and possibilities on this matter: compound-coerce to go from `A` to `Z` indirectly along multiple different paths, they should all have the same end result. - * It should be unambiguous, or preserve the meaning of the input: - `impl From for u32` as `x as u32` feels right; as `(x as u32) * 12345` - feels wrong, even though this is perfectly pure, total, and injective. What - this means precisely in the general case is unclear. - * It should be lossless, or in other words, injective: it should map each observably-different element of the input type to observably-different elements of the output type. (Observably-different means that it is possible @@ -391,8 +385,13 @@ Some further thoughts and possibilities on this matter: modulo things that "shouldn't count" like observing execution time or resource usage.) + * It should be unambiguous, or preserve the meaning of the input: + `impl From for u32` as `x as u32` feels right; as `(x as u32) * 12345` + feels wrong, even though this is perfectly pure, total, and injective. What + this means precisely in the general case is unclear. + * The types converted between should the "same kind of thing": for instance, - the *existing* `impl From for Ipv4Addr` is pretty suspect on this count. + the *existing* `impl From for Ipv4Addr` feels suspect on this count. (This perhaps ties into the subtyping angle: `Ipv4Addr` is clearly not a supertype of `u32`.) @@ -566,8 +565,8 @@ below.) The `translate` method says that it should be possible to translate to any *other* `ResultCarrier` type which has the same `Normal` and `Exception` types. This may not appear to be very useful, but in fact, this is what can be used to -inspect the result, by translating it to a concrete type such as `Result` and then, for example, pattern matching on it. +inspect the result, by translating it to a concrete, known type such as +`Result` and then, for example, pattern matching on it. Laws: From fd358fba362af5c4e893ba62348e0639b174647b Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Fri, 22 Jan 2016 10:41:23 -0800 Subject: [PATCH 9/9] Add specifics around feature gates --- active/0000-trait-based-exception-handling.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/active/0000-trait-based-exception-handling.md b/active/0000-trait-based-exception-handling.md index 0d868aaaee4..507af12c767 100644 --- a/active/0000-trait-based-exception-handling.md +++ b/active/0000-trait-based-exception-handling.md @@ -313,6 +313,12 @@ Without any attempt at completeness, here are some things which should be true: (In the above, `foo()` is a function returning any type, and `try_foo()` is a function returning a `Result`.) +## Feature gates + +The two major features here, the `?` syntax and the `try`/`catch` +syntax, will be tracked by independent feature gates. Each of the +features has a distinct motivation, and we should evaluate them +independently. # Unresolved questions