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.
This commit is contained in:
Gábor Lehel 2015-12-29 22:10:42 +01:00
parent 63a3c4d088
commit ab63c4fc3a
1 changed files with 285 additions and 394 deletions

View File

@ -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.
* 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".
* (Potentially) a `throw` operator, and `throws` sugar for function signatures.
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<Foo, Bar>`, 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<Foo, Bar>`, 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.
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 the `try`..`catch` theme, each of which is more
convenient in different circumstances.
There are two variations on this theme:
1. `try { EXPR } catch IRR-PAT { EXPR }`
1. `try { 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.
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.
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`.
## 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<Other: Carrier<Normal=Normal, Exception=Exception>>(from: Self) -> Other;
}
This trait basically just states that `Self` is isomorphic to
`Result<Normal, Exception>` 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<Normal, Exception>` 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<T, E> Carrier for Result<T, E> {
type Normal = T;
type Exception = E;
fn embed_normal(a: T) -> Result<T, E> { Ok(a) }
fn embed_exception(e: E) -> Result<T, E> { Err(e) }
fn translate<Other: Carrier<Normal=T, Exception=E>>(result: Result<T, E>) -> 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<T> Carrier for Option<T> {
type Normal = T;
type Exception = ();
fn embed_normal(a: T) -> Option<T> { Some(a) }
fn embed_exception(e: ()) -> Option<T> { None }
fn translate<Other: Carrier<Normal=T, Exception=()>>(option: Option<T>) -> 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<Other: Carrier<Normal=(), Exception=()>>(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,47 +199,18 @@ are merely one way.
Shallow:
'here: {
Carrier::embed_normal(foo()?.bar())
Ok(foo()?.bar())
}
Deep:
'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())
}
* 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 {
@ -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<Car: Carrier<Normal=B, Exception=C>>(A) -> Car {
try {
'fn: {
CODE
}
}
}
Deep:
fn foo<Car: Carrier<Normal=B, Exception=C>>(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<T>`) = repr(`Result<T, ()>`)
* 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
# 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`?)
* Wait (and hope) for HKTs and generic monad sugar.
# Appendices
# Future possibilities
## Alternative formulations of the `Carrier` trait
## 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<T>` is completely equivalent to `Result<T, ()>` 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<Normal, Exception>`.
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<Other: ResultCarrier<Normal=Normal, Exception=Exception>>(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<Normal, Exception>` 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<T, E> ResultCarrier for Result<T, E> {
type Normal = T;
type Exception = E;
fn embed_normal(a: T) -> Result<T, E> { Ok(a) }
fn embed_exception(e: E) -> Result<T, E> { Err(e) }
fn translate<Other: ResultCarrier<Normal=T, Exception=E>>(result: Result<T, E>) -> 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<T> ResultCarrier for Option<T> {
type Normal = T;
type Exception = ();
fn embed_normal(a: T) -> Option<T> { Some(a) }
fn embed_exception(e: ()) -> Option<T> { None }
fn translate<Other: ResultCarrier<Normal=T, Exception=()>>(option: Option<T>) -> 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<Other: ResultCarrier<Normal=(), Exception=()>>(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`
* 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`.
* 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:
* repr(`Option<T>`) = repr(`Result<T, ()>`)
* 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.
### 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<Normal, Exception>) -> Self;
fn to_result(Self) -> Result<Normal, Exception>;
@ -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;