Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: error conventions and syntactic support (including changes to macro syntax) #204

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions active/0000-error-conventions-and-sugar.md
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
Copy link
Member

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!

Copy link
Member Author

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.

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()`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 I like unwrap being a bit unergonomic

Copy link
Member Author

Choose a reason for hiding this comment

The 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)`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like the ? operator for this. I proposed something similar a long time ago on the mailing list and it was fairly unpopular. The consensus seemed to be towards something more general like Haskell's do syntax. Personally, I still like the ?operator.

Copy link
Member

Choose a reason for hiding this comment

The 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 return built-in to the language. (I don't know if this is a good thing or a bad thing... but it is certainly a point of non-orthogonality.)

Copy link
Member

Choose a reason for hiding this comment

The 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 `!`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 we use @ for variable binding in patterns, so it would be ambiguous (for humans, definitely, for the parser, maybe) for macros. Also, I like ! for macros, it reminds the user that they are dangerous.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 ! for macros as we have today is nice, but the pairing of ! and ? for error handling is also appealing (to me, at least).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One alternative would be not doing the ! behaviour. :)

Copy link
Contributor

Choose a reason for hiding this comment

The 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 @println is any better or worse than println! as a marker.

As far as the @ syntax in patterns, I think any ambiguity there is easily resolved by saying that @foo is a specific token, and hence spaces can resolve the ambiguity. (We did do a bikeshed some time back and completely fail to find an appealing alternative to @.) Anyway, @ is a marginal feature, so I would not allow this concern to derail the larger proposal by itself.


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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just be strongly and actively persuing non-unwrap solutions; e.g. the equivalent function in Haskell (fromJust) is very rarely used. Why isn't this the case in Rust? Can we move into that space, rather than just catering to what we currently have?

Copy link

Choose a reason for hiding this comment

The 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 fromJust.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@huonw Note also that Haskell has a semi-equivalent to fail! called error: http://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#v:error

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 error rather than a Maybe return type. I suspect this is why fromJust is uncommon.

In other words, unwrapping an Option/Result for us is justified when the value represents a potential contract violation, and you're choosing to assert that you've met the contract. If instead contract failure always results in failure, unwrap would become more rare, but you'd still fail in the same cases! (It's just that this would be less explicit in the client code.)

abuse. There are a few counterpoints:

* The current ergonomics have led to APIs failing *internally* so that their
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have examples of this?

Copy link
Member

Choose a reason for hiding this comment

The 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 Result for their methods, they have failing and non-failing variants of functions.

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
Copy link
Member

Choose a reason for hiding this comment

The 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 foo()! to satisfy the compiler (or because it appears something should be true, without properly thinking about it), rather than actually properly thinking about it and handling the error, and then, later, when reviewing/tracking down a bug, the ! can easily be lost as line noise.

Further, there's not a huge difference between ! and ?, possibly leading to situations like "Am I meant to be asserting this rather than propagating?" (and vice versa).

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:
Copy link
Member

Choose a reason for hiding this comment

The 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 ? example nicer under the modifier-on-identifiers format:

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 ...?; very visually jarring. . .


```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"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's really a mish-mash of map and and_then, since multiple levels of option are flattened.


```rust
foo(x, y)?.bar(z)?.baz // sugared version
Copy link
Member

Choose a reason for hiding this comment

The 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 foo(x, y)?bar(z)?baz


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))
```

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that you can do the following instead:

foo(x, y).and_then(|t1|  t1.bar(z))
         .map(|t2|  t2.baz)

An alternative idea is to rename and_then() into then() (or even into ?()) and perhaps introduce some sugar for simple closures:

try!(foo(x, y).then( #.bar(z) ).map( #.baz ))

Copy link
Contributor

Choose a reason for hiding this comment

The 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. |ref x|. There are just too many things to specify.

While I think using # for "simple closure" sugar is alright (Clojure does this), trying to add sugars to all usage patterns of Rust closures may result in sigil soap, and adding sugars to only some "common" patterns may result in inconsistency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, only the Fn, FnMut and FnOnce choice is 100% important here. Capturing by reference is just capturing a reference by value (the ref |...| ... syntax is sugar for capturing references), and |ref x| is not particularly relevant, it's the same was writing |x| ... &x; it's just taking a reference to one of the closure's arguments.

It may be possible to infer the most general closure trait based on how captured variables are used (try Fn, then FnMut and then FnOnce as a "last resort"), and thus sugar like this could be feasible without requiring a sigil soup.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@huonw @nielsle , what I have in mind is something like this:

#{#1.foo()} = |x, **| { x.foo() } = |&mut: x, **| { x.foo() }
#:{#1 + #2} = |: x, y, **| { x + y }
#&{# * #3} = |&: x, y, z, **| { x * z }

EDIT: the ** part means "possible additional unused arguments".

Basically, #{...} introduces a simple closure implementing FnMut.
#&{...} is a simple Fn closure.
#:{...} is a simple FnOnce closure.

Inside these closures, #1, #2, ..., is the notation for the (not declared) positional arguments. #1 can also be simply #. (#0 is "the closure environment", but we may forbid its usage.)

The closures can only have one expression inside like python lambdas, and nested simple closures are not allowed. (You cannot nest #{...} inside another #{...}, as it is not clear what the inner #1, #2s would be referring to.)

But you can use attributes inside a simple closure. Like #{ #[foo] #1.baz()}.

I took a page from Clojure, and I love the #(...) closure sugar there.

But there may be problems with type inference and method look-ups. Clojure doesn't care too much about a closure's arity (#(+ &1 &2) is variadic there) . But Rust do care, and we are going to gain multiple dispatch, which AFAIK, will make it possible to "overload" a method like Foo::bar(&self, closure: [ClosureType]) for different closure types differing in arities, argument types and env passing styles.

How do we infer that? (Of course, when we cannot infer things, we can just ask the programmer to use the "full" closure notations.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@huonw @nielsle:

Or even better,

Currently we have the following "closure headers": | ... |, |: ...|, |&: ...| and |&mut: ...|.

We can add three more: #|, #|: and #|&:.

Hello sigil soup!

Then

foo(x, y).and_then(|t1|  t1.bar(z)).map(|t2|  t2.baz)

would be

foo(x, y).then(#| #.bar(z)).map(#| #.baz))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought #{...} and #| don't seem to be an obvious improvement ... (At least in this use case.)

But "directly" sugared expressions like foo.map( #.bar ) is too special-cased for my taste.

Copy link

Choose a reason for hiding this comment

The 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:
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try! already has an ergonomic and documentation advantage over map; would a measurement tell you anything more than that people use things that are easier and better documented? Using this as evidence that try! should be further advantaged seems circular.

propagating errors, but it might be worthwhile to measure the usage compared to
`map`.