-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: error conventions and syntactic support (including changes to macro syntax) #204
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
- Start Date: (fill me in with today's date, 2014-08-15) | ||
- RFC PR #: (leave this empty) | ||
- Rust Issue #: (leave this empty) | ||
|
||
# Summary | ||
|
||
This RFC sets out a vision for refining error handling in Rust, in two | ||
parts: | ||
|
||
* A stronger set of conventions around `fail!` and `Result` | ||
* Sugar to make working with `Result` highly ergonomic | ||
|
||
In a nutshell, the proposal is to isolate uses of `fail!` to an extremely small | ||
set of well-known methods (and assertion violations), with all other kinds of | ||
errors going through `Result` for maximal flexibility. Since the main downside | ||
of taking this extreme position is ergonomics, the RFC also proposes notation | ||
for consuming `Result`s: | ||
|
||
* Change macro invocation syntax from `macro_name!` to `@macro_name`. | ||
* Use `foo!` for today's `foo.unwrap()` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. -1 I like unwrap being a bit unergonomic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See "Drawbacks" for a discussion of this perspective. |
||
* Use `foo?` for today's `try!(foo)` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find it really weird that we would essentially have two names for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Oh, and it's a really subtle marking that changes control-flow; it's not so bad because of the types, but still a little weird.) |
||
|
||
While the two parts of this proposal reinforce each other, it's possible to | ||
consider each of them separately. | ||
|
||
# Motivation | ||
|
||
Rust has been steadily moving away from task failure as a primary means of error | ||
handling, and has also discouraged providing both `fail!` and `Result` variants | ||
of methods. However, it is very difficult to craft a clear set of guidelines | ||
that clearly says when `fail!` is appropriate, and so the libraries remain | ||
inconsistent in their error signaling approach. | ||
|
||
(The draft guidelines [here](http://aturon.github.io/errors/signaling.html) are | ||
an attempt to capture today's "rules", but are not clear-cut enough to resolve | ||
disputes about uses of `fail!`.) | ||
|
||
The main challenge is dealing with "programmer errors" or "contract violations" | ||
in APIs -- things like out-of-bounds errors, unexpected interior nulls, calling | ||
`RefCell::borrow` on a mutably-borrowed value, and so on. In today's libraries, | ||
the API designer can choose whether to treat usage errors as assertion | ||
violations (and hence `fail!`) or as permitted (and hence return a useful `Err` | ||
value). The problem is that "programming error" is often in the eye of the | ||
beholder, and even in the case of things like array indexing there are useful | ||
patterns based on returning a `Result` rather than `fail!`ing. | ||
|
||
The goal of this RFC is to lay out a vision for an extreme but ergonomic | ||
position on error signaling that would support a clearer set of guidelines and | ||
therefore more consistent library APIs. | ||
|
||
# Detailed design | ||
|
||
The proposal has two pieces. First, a set of clear-cut conventions on when to | ||
use `fail!`. Second, since `fail!` is often used for ergonomic reasons, a | ||
proposal for making `Result` easier to work with. | ||
|
||
## Error conventions | ||
|
||
The use of `fail!` is restricted to: | ||
|
||
* Assertion violations (`assert!`, `debug_assert!`, etc.), which should *not* be | ||
used for input validation. | ||
|
||
* Unwrapping an `Option` or `Result` (which will need to be renamed; see below) | ||
|
||
* Out-of-bounds or key-not-found errors when using *sugared notation* for | ||
indexing `foo[n]` (or the proposed | ||
[slicing notation](https://github.com/rust-lang/rfcs/pull/198)). (As opposed | ||
to "normal" methods like `get`; see below.) | ||
|
||
* Perhaps a few others, TBD. | ||
|
||
All other errors, be they contract violations on inputs or external problems | ||
like file-not-found should use a `Result` (or in limited cases, `Option`) for | ||
error signaling. | ||
|
||
In particular, collections will offer methods like `get` that work like indexing | ||
but return an `Option` for signaling out-of-bounds or key-not-found. | ||
|
||
The result of these conventions is that: | ||
|
||
1. Failure is very clearly marked (easily grepped for) and under the client's | ||
control. This allows clients to take advantage of built-in error checking | ||
provided by an API without having to cope with task failure. | ||
2. API designers have extremely clear guidelines on when to `fail!`. | ||
|
||
### Tangent: renaming `unwrap` | ||
|
||
At the moment, `unwrap` is used for `Option`/`Result` (where it can fail) as | ||
well as other types (where it cannot fail). These must be renamed apart if we | ||
want failure to be clearly signaled. Some proposals include: | ||
|
||
* Rename the `Option`/`Result` versions to `assert_some` and | ||
`assert_ok`/`assert_err`. | ||
|
||
* Rename the `Option`/`Result` versions to `expect`/`expect_err`, and rename | ||
`Option::expect` to `expect_msg`. | ||
|
||
* Rename other (non-`Option`/`Result`) uses of `unwrap` to `inner` or `into_inner`. | ||
|
||
If we adopt the shorthand syntax suggested below, we could cope with a much | ||
longer name, such as `unwrap_or_fail`. | ||
|
||
The advantage of having `assert` in the name is a clearer signal about possible | ||
`fail!` invocation, but | ||
[many feel](https://github.com/rust-lang/rust/pull/16436) that | ||
newcomers are likely to be surprised that `assert` returns a value. | ||
|
||
The specific proposal here will need to be pinned down before the RFC is | ||
finalized or accepted, but I want to open the floor to discussion first. | ||
|
||
## Ergonomics for error handling | ||
|
||
Many operations that currently use `fail!` on bad inputs do so for ergonomic | ||
reasons -- perhaps bad inputs are rare, and the API author wants to avoid a lot | ||
of `.unwrap` noise. | ||
|
||
To help relieve this ergonomic pressure, we propose three syntax changes: | ||
|
||
1. Macro invocation is written with a leading `@` (as in `@println`) rather than | ||
trailing `!` (as in `println!`). This frees up `!`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. -1 we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any alternative suggestions? At one point macros were invoked with a leading #, but I think many disliked that notation. I agree that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One alternative would be not doing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree that macros are any more "dangerous" than any other user feature. Dangerous to me is a fairly provocative term that implies unsafety. Perhaps you mean "non-obvious"? In that case, I don't see why As far as the |
||
|
||
2. The "unwrap" method (whatever it ends up being called) can be invoked via a | ||
postfix `!` operator: | ||
|
||
```rust | ||
// under above conventions, borrow would yield an Option | ||
String::from_utf8(my_ref_cell.borrow()!)! | ||
|
||
// Equivalent to: | ||
String::from_utf8(my_ref_cell.borrow().unwrap()).unwrap() | ||
``` | ||
|
||
3. The `try!` macro can be invoked via a postfix `?` operator: | ||
|
||
```rust | ||
use std::io::{File, Open, Write, IoError}; | ||
|
||
struct Info { | ||
name: String, | ||
age: int, | ||
rating: int | ||
} | ||
|
||
fn write_info(info: &Info) -> Result<(), IoError> { | ||
let mut file = File::open_mode(&Path::new("my_best_friends.txt"), Open, Write)?; | ||
file.write_line(format!("name: {}", info.name).as_slice())?; | ||
file.write_line(format!("age: {}", info.age).as_slice())?; | ||
file.write_line(format!("rating: {}", info.rating).as_slice())?; | ||
Ok(()); | ||
} | ||
``` | ||
|
||
The `!` and `?` operators would bind more tightly than all existing binary | ||
or unary operators: | ||
|
||
```rust | ||
// The following are equivalent: | ||
foo + !bar! | ||
foo + (!(bar!)) | ||
``` | ||
|
||
It is common for unary operators to bind more tightly than binary operators, and | ||
usually unwrapping/propagating a `Result` is the innermost step in some compound | ||
computation. | ||
|
||
# Drawbacks | ||
|
||
An obvious drawback is that `println!` looks lighter weight than `@println` (or, | ||
in any case, we're all quite used to it). On the other hand, the `!` and `?` | ||
pairing for error handling seems very appealing, and has | ||
[some precedent](https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/Swift_Programming_Language/OptionalChaining.html) | ||
(note however that Swift's `?` notation works different from that being proposed | ||
here; see Alternatives). | ||
|
||
Some people feel that unwrapping should be _un_ergonomic, to prevent | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should just be strongly and actively persuing non- There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the other hand, Haskell allows you to have non-exhaustive pattern matches and this makes it very easy to just write partial functions instead of dealing with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @huonw Note also that Haskell has a semi-equivalent to They face the same conventions difficultly being addressed here: what should happen on contract violation? I think in many cases (taking the head of an empty, out of bounds errors, etc) they choose to use In other words, unwrapping an |
||
abuse. There are a few counterpoints: | ||
|
||
* The current ergonomics have led to APIs failing *internally* so that their | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have examples of this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Channels and RefCell are two primary examples of this. Due to the ergonomics of returning a |
||
clients don't have to `unwrap` -- leading to *more* task failure, not less. | ||
|
||
* If the above conventions are adopted, `Result`/`Option` will be used in many | ||
cases to signal the possibility of contract violation. Unwrapping is then just | ||
an assertion that the contract has, in fact, been met. With the overall | ||
proposal, programmers will *clearly know* when and where a contract is being | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is true, us programmers are a stupid bunch. It's very easy to get sucked into just writing Further, there's not a huge difference between |
||
checked, but the assertion is lightweight. | ||
|
||
* By placing `try!` and `unwrap` on equal footing via a simple and clear marker, | ||
programmers are both aware of the potential for errors and easily able to | ||
choose between two extreme ways of handling them: failing immediately, or | ||
passing them on. | ||
|
||
|
||
# Alternatives | ||
|
||
## Piecing apart the proposal | ||
|
||
At a coarse grain, we could: | ||
|
||
* Stick with the status quo, where using `fail!` on contract violation is an API | ||
designer's choice. | ||
|
||
* Just change the conventions as proposed, without adding sugar. Many have | ||
expressed concern about the ergonomics of such an approach. | ||
|
||
* Just add the `!` and `?` sugar, without setting firmer conventions about | ||
`fail!`. This would be an improvement over the status quo, but also a missed | ||
opportunity. Without extremely clear guidelines about error signaling and | ||
handling, we risk | ||
[fragmentation](http://www.randomhacks.net/2007/03/10/haskell-8-ways-to-report-errors/). | ||
|
||
## Syntax alternatives | ||
|
||
### Tying `!` and `?` to identifiers | ||
|
||
An alternative to making `!` and `?` work as postfix operators would be to treat | ||
them as modifiers on identifiers. For example, rather than writing this: | ||
|
||
```rust | ||
foo! // unwrap a variable | ||
self.foo! // unwrap field foo of type Option<T> | ||
self.foo(x, y, z)! // invoke a Result-returning method and unwrap | ||
foo(x)!.bar(y)!.baz! // method and field-access chaining | ||
``` | ||
|
||
you could instead imagine writing this: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find the below far more visually appealing than the above. Likewise, I find the fn write_info(info: &Info) -> Result<(), IoError> {
let mut file = File::open_mode?(&Path::new("my_best_friends.txt"), Open, Write);
file.write_line?(format!("name: {}", info.name).as_slice());
file.write_line?(format!("age: {}", info.age).as_slice());
file.write_line?(format!("rating: {}", info.rating).as_slice());
Ok(());
} Maybe I just find the series of lines ending with |
||
|
||
```rust | ||
foo! // unwrap a variable | ||
self.foo! // unwrap field foo of type Option<T> | ||
self.foo!(x, y, z) // invoke a Result-returning method and unwrap | ||
foo!(x).bar!(y).baz! // method and field-access chaining | ||
``` | ||
|
||
Arguably, `foo!(x).bar!(y)` reads better than `foo(x)!.bar(y)!`, and the extra | ||
flexibility of a general postfix operator is problably not needed. On the other | ||
hand, postfix operators are simpler and more familiar syntactic forms. | ||
|
||
### `?` as `map` | ||
|
||
In the above proposal, the `?` operator is shorthand for a use of the `try` | ||
macro. An alternative, used in the | ||
[Swift language](https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/Swift_Programming_Language/OptionalChaining.html) | ||
among others, is to treat `?` as shorthand for `map` (called "option chaining"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe it's really a mish-mash of |
||
|
||
```rust | ||
foo(x, y)?.bar(z)?.baz // sugared version | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Groovy, you don't need the dots, which I like, i.e., your example becomes |
||
|
||
try!(try!(foo(x, y)).bar(z)).baz // this RFC's interpretation | ||
|
||
foo(x, y).map(|t1| // Option-chaining alternative | ||
t1.bar(z).map(|t2| | ||
t2.bar)) | ||
``` | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that you can do the following instead:
An alternative idea is to rename
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nielsle, with unboxed closures implemented, Rust has three kinds of closures, and we have the choice to do by-value or by-ref captures, and when we capture an upvar by value, we can still use their references inside the closure body, e.g. While I think using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, only the It may be possible to infer the most general closure trait based on how captured variables are used (try There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @huonw @nielsle , what I have in mind is something like this:
EDIT: the Basically, Inside these closures, The closures can only have one expression inside like python lambdas, and nested simple closures are not allowed. (You cannot nest But you can use attributes inside a simple closure. Like I took a page from Clojure, and I love the But there may be problems with type inference and method look-ups. Clojure doesn't care too much about a closure's arity ( How do we infer that? (Of course, when we cannot infer things, we can just ask the programmer to use the "full" closure notations.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a second thought But "directly" sugared expressions like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps this discussion should go to http://discuss.rust-lang.org/ . We seem to be diverging from the original RFC :-) |
||
Both interpretations of `?` work similarly to monadic `do` notation: | ||
|
||
* You write a chain of computations as if every operation succeeds. | ||
|
||
* An error at any point aborts the "rest of the computation" and returns the | ||
`Err`. | ||
|
||
The difference between the `try!` and `map` interpretation is just what counts | ||
as "the rest of the computation". With `try!`, it is the entire function, while | ||
with `map` it is the rest of the expression. | ||
|
||
(Note that Rust's `io` module is built with implicit `map`-like semantics: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (FWIW, I'm not 100% sure this is a good thing... It certainly leads to nice code for small snippets like the below, but can lead to strangely delayed error handling for larger code, where an IO object is not accessed immediately.) |
||
errors are silently propagated in expressions like | ||
`File::open(some_file).read_to_end()`, so that errors on opening *or* reading | ||
both just return `Err` for the whole expression.) | ||
|
||
Anecdotally, `try!` seems to be the most important and common means of | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
propagating errors, but it might be worthwhile to measure the usage compared to | ||
`map`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to this, we seem to be doing it anyway, and I like it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, there's not complete agreement about this convention, and there are still a wide range of functions in
libstd
that can fail on contract violation.