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

Conversation

aturon
Copy link
Member

@aturon aturon commented Aug 18, 2014

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 Results:

  • Change macro invocation syntax from macro_name! to @macro_name.
  • Use foo! for today's foo.unwrap()
  • Use foo? for today's try!(foo)

While the two parts of this proposal reinforce each other, it's possible to
consider each of them separately.

Rendered

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.

@CloudiDust
Copy link
Contributor

I agree that we need aggressive guidelines on error handling, and the proposed ones are fine.

Personally I prefer something like the monadic do notation to other ad-hoc syntax sugars.

Or is it possible to go the other way around, making sure that the syntax sugars here are extensible to other monads?

* 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).

@ftxqxd
Copy link
Contributor

ftxqxd commented Aug 18, 2014

(Warning: enormous comment. I’m sorry. 😦)

👍 I really like this proposal—I believe that error-handling (with Result, Option etc.) needs to be a more first-class part of the language. It’s also great to see that a relatively big change (one that significantly changes syntax) is being proposed at this time. Because 1.0 is approaching, Rust really needs to sort out its error-handling and other major parts of the language, and this RFC does that incredibly well. We don’t want to end up with error-handling in Rust being incredibly messy and not be able to do anything about it.


There is a recent trend towards removing uses of unwrap, but I think that having a short way of unwrapping things is better than either using the unwrap method or providing two functions which do the same thing except where one fails and the other returns Result or Option. The latter isn’t really common in Rust code, but it’s still neater than sprinkling unwraps everywhere when the code can be guaranteed not to fail. I wouldn’t mind too much if the ! part of this proposal were removed (and therefore macros could remain the same), but I’d still prefer it to stay.

I especially like the symmetry between ! and ?—they both stop function execution, but ? is recoverable while ! isn’t. I really hope that ? paired with Result will make error-handling in Rust even nicer than in languages that use exceptions (with try/catch). When people ask how to do errors in Rust, a common response right now is ‘use fail!’, whereas I hope in the future that failure will be relatively rare (as this RFC proposes) and the response to that question will be ‘use Result and ?’, with failure reduced to a mere footnote referring to division by zero, out-of-bounds indexing and the like.


I also prefer the alternative for tying ! and ? to identifiers—it’s a lot neater, and AFAIK doesn’t lose any flexibility (except in places where they almost certainly wouldn’t be used, e.g. operators). It places the operator nearer the main operation rather than hiding it at the end: @write?("looooong message...") is a lot clearer that it could return from the function than @write("looooong message...")?, and I can imagine that it could be easy to forget the ? at the end, whereas putting it earlier makes it much easier to remember. Also, try! right now is almost always used as its own statement. That would mean that semicolons would almost always follow ?s. ?; doesn’t look very pretty.


Regarding macros, I don’t particularly like the syntax proposed in the RFC. I’d actually prefer macros to be called with a # prefix (e.g., #println("hello world")). I believe Rust actually had this once, so I’d like to know why it was changed—it makes a nice symmetry between attributes and macros, which are quite similar. (A lot of the time, attributes are really just fancy macros.) Alternatively, macros could be completely un-delimited—I’ve heard a lot of newcomers to Rust complaining about how the ! is necessary for macros. I don’t agree with this, because there are a ton of complexities that come with this (such as methods with the same name as a macro), but it’s an option.


One last thing: the RFC needs to be clearer about how the new operators are defined. I’d presume that two new built-in traits would be added, but how would the trait for ? be declared? (Previously it was a macro, but obviously that wouldn’t work with a trait.) Also, would ? be defined for Option? If so, would it require the function to return Option<T> or Result<T, ()>?


TL;DR: Great proposal. ! is great for when the value is known to be Some/Ok (which is quite common, really). ? and ! should be bound to identifiers, and use # for macros.

@chris-morgan
Copy link
Member

I share @huonw’s concerns and am worried about baking these particular semantics in in such a specific way when they may not even be the operations we wish people to use the most often. And having ? return will be a very surprising thing to newcomers, and may even trick oldies briefly. But then again, try! is already rather like that…

How about something like this as a replacement for let b = try!(a);:

let Ok(b) = a else return;

I acknowledge that that syntax would suggest the type Result<T, E> for a’s T rather than the function’s T. Either “something” could be done about it, or else it could be made explicit:

let Ok(b) = a else Err(e) => return Err(e);

Hmm, I see this isn’t ending up with anything but more verbosity…


@P1start: although I suspect that a trait is less likely to be what is intended (though I do think it’d be better as a trait than as a language item applying specifically to Option and/or Result), it could be implemented as a trait with a special enum, having the method return enum Something<V, R> { Value(V), Return(R) } and have the compiler desugar and/or transform it all as desired.

@bachm
Copy link

bachm commented Aug 18, 2014

Regarding the ergonomics portion of this RFC. Using macros for making common operations involving Option / Result more ergonomic is a sign that the language is lacking something. Changing macro invocation syntax just for this purpose, and using sigils instead of methods or keywords is also something I'm not enthusiastic about. I think the RFC is going down the wrong path.

@SiegeLord
Copy link

Macros are omnipresent in Rust, and will continue to be omnipresent to the extent that the features they replace (keyword/variadic arguments, chaining, overloading, etc) are not added to the language proper. Anything that makes their invocation uglier is a non-starter to me.

I think that adding this ! sugar to failing is counter-productive. From my POV, the counter-arguments for it are flawed:

The current ergonomics have led to APIs failing internally so that their clients don't have to unwrap -- leading to more task failure, not less.

In my opinion, this is better solved by having failing + non-failing variants. The fact that the style guide discourages the combination is what contributes to the API's becoming fail-only. By making Result handling more ergonomic, the API's may choose the non-fail variant.

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.

This is an anti-pattern. The style guide itself suggests using a failing-API entry for the cases where the contract can be trivially checked by the calling code.

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.

This makes it seem both extremes are equally good or desirable.

I also want to reiterate other's concern about ? implying an implicit return. With try! you at least know that it's a macro, and it can do whatever it feels like.

Lastly, I don't agree with adding special syntax sugar to begin with, even if I agree that Result wrangling is un-ergonomic. In particular, Option chaining is very un-ergonomic as well, and I seem to be using it often too... why does it not get sugar? I'd rather have a more general solution to this chaining issue.

And finally... it is August today. In 4 months 1.0 is scheduled to be released. These syntactic changes should be considered very carefully because they simply won't get too much testing before they are set in stone. There are plenty of examples in Rust with some syntax getting reverted/changed after the initial iterator many months/years after the initial introduction... and that syntax is usually for a core language feature and not just sugar (but take do for example). Just because this sugar may seem like a good idea now, it might not make any more sense in the future. What if HKT + real monadic do notation is added? This sugar may seem very silly then.

@aturon aturon changed the title RFC: error conventions and sugar RFC: error conventions and syntactic support (including changes to macro syntax) Aug 18, 2014
@ftxqxd
Copy link
Contributor

ftxqxd commented Aug 19, 2014

In my opinion, this is better solved by having failing + non-failing variants.

Would you really prefer an API to be forced to expose two separate methods that do the same thing for every method that doesn’t always succeed? The purpose of the first part of the RFC is to standardise methods to always return Option or Result in case of failure, and the second part provides an easy way to turn a non-failing method into a failing one, removing the motivation for failing methods. You could consider adding ! after a method invocation to be a way of turning a Result-based method into a failing one automatically.

I understand the concerns about ? returning from the enclosing function, but I like to view it in a different way that makes it seem much better IMO. You could pretend that a function returning result:

fn foo(...) -> Result<T, E> { ... }

is actually a function that can throw an exception:

fn foo(...) -> T throws E { ... }

With this analogy in mind, a call like foo(...) is a way of explicitly handling the error (by pattern-matching on Result), and a call like foo(...)? is a way of not handling the error at all, and thereby passing it down to the calling function. This is the opposite of exceptions in most languages, where exceptions are implicitly passed down, and need to be explicitly handled (with try/catch). This way (with explicit passing-down) makes handling errors the ‘default’ thing to do, and passing errors down (which is quite common, but not always the best thing to do) must be done explicitly. ! fits into this analogy by being a way of stopping the error from ‘infecting’ the function with a Result type when it can be assured that it’s not going to fail.

In particular, Option chaining is very un-ergonomic as well, and I seem to be using it often too... why does it not get sugar?

I agree that there’s no reason to restrict this to Results. The RFC sort of implies that ! works for Option too, and I also sort of assume that ? works for it too, but it really does need to be clarified how this would be extended to other custom types. Making Result and Option lang items isn’t really an option.

I don’t, however, agree that adding monadic do would be a solution to this problem. do works relatively well in Haskell, where functions consist of single expressions. However, in Rust, functions aren’t so simple and are typically full of various control structures. Monadic do does not usually (and AFAIK, cannot) extend to these structures. Something like this:

fn foo() -> Result<T, E> {
    do {
        let x <- bar());
        for i in x {
            do {
                let y <- baz(i);
                println!("{}", y);
            }
        }
    }
}

is not very pretty. Monadic do is normally restricted to a small set of statements: binding (var <- expr) and raw expressions (expr, equivalent to _ <- expr). Somehow extending this to things like for loops, if possible, just means a whole bunch of special cases, and in any case it’s still unergonomic: foo(try!(bar())) would require a temporary variable: let x <- bar(); foo(x);. Even if something like this were resolved, I’d guess that what you’d end up with would be this proposal but delimited by do blocks instead of functions. (For monads that aren’t used for error handling, I think that Rustaceans probably shouldn’t use those with do syntax anyway. People are reluctant about adding a Monad HKT trait anyway because it could make people think that Rust is a confusing functional language, and promoting the highly-functional cryptic style of things like list monads with do notation is even worse.)


I also had an unrelated thought about ! and ?: what if, with fn foo(..args) -> Result<T, E>, foo? would automatically create a closure equivalent to |..args| foo(..args)?? This would be extended to anything implementing Fn<Args, R> where R: Questionmarkable<T, E> (along with all the other Fn* traits). This would mean that not only would foo?(bar) and foo(bar)? be possible, but also just a plain foo? that creates a closure that unwraps the return value of the function. This would also let them still be normal postfix operators. The main problem with this is that at the moment taking values of methods isn’t allowed for some reason. Does UFCS fix this?

@pythonesque
Copy link
Contributor

I would like to chime in and say that I, too, am not a fan of adding sugar like this prior to 1.0. The other part of the proposal (reduce uses of fail! in the standard library) is great :)

@SiegeLord
Copy link

Would you really prefer an API to be forced to expose two separate methods that do the same thing for every method that doesn’t always succeed?

My point is that the goal should be to make Result handling so ergnomic that you never want to turn it into a failure by calling unwrap. Failure should be reserved for cases when even those changes aren't sufficient. E.g. consider operator overloading... those pretty much have to fail unless you want to completely lose the sugar... and once you lose the sugar, you might as well provide an alternate method.

That said, it'd be great if this somehow extended to operator overloads, so you could just sprinkle ? everywhere and have things sort of work. Currently, though, if I understand it correctly it'd look like this ((a + b)? + c)? + d)? which seems less than ideal.

@pczarn
Copy link

pczarn commented Aug 19, 2014

Currently we can grep for \bassert[_\b]|[_\b]fail!?\(|\bunwrap\( to find the majority of direct points of failure. This would become unfeasible with the foo! sugar, whereas renaming unwrap to assert* or *_or_fail would make this regex even simpler. I agree that the ! can be lost as noise.

@glaebhoerl
Copy link
Contributor

I've long considered foo!() for macro invocation to be one of Rust's most inspired syntactic choices, and frankly, I don't think I would want to give it up for anything.

I think the general aim of making error handling and Option and Result manipulation more ergonomic is a good one, along with adding syntax sugar to assist in doing so, but I don't feel like this is the right sugar. ! and ? are very valuable "real estate", and this proposed use of them is very narrowly tailored. The current practice with Results and try! (and unwrap()) feels to me like an interim "least bad" solution until we figure out a better one, not the right solution itself. So I'm not a big fan of baking it into the language using valuable syntax as this RFC proposes. I would rather keep punting on it until we think of better ideas (perhaps as the language gains new capabilities), and try to get it right. (As a specific point, I don't think fail!() for contract violations is all that bad, especially given that in Rust (unlike some other languages), you can even handle it at task boundaries. Having both failing and Result/Option-returning variants when a contract violation is in the eyes of the beholder doesn't seem like the end of the world, either.)

(The let Ok(b) = a else return; feature @chris-morgan mentions (explicit refutable let) is something I've also suggested before, and I still think it's a good idea. However, it's not clear to me how much it would contribute to alleviating the same burdens which this RFC aims to. Perhaps someone else has a clearer picture?)

Brainstorming: The general drift of this RFC, together with the other one, feels to me a bit like we're trying to re-encode the usual exception handling and propagation etc. logic using Result and various ad-hoc mechanisms and syntactic sweeteners. Perhaps it might be worthwhile to explore ideas in that direction more directly? (I have some tentative ideas which seem promising, but as they literally just occurred to me over the past half hour, I want to let them settle a bit more before trying to lay them out.)

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

@lilyball
Copy link
Contributor

I'm inclined to say that what we really should do is simply extend the identifier rules to allow for a trailing ! and adopt a convention that methods that can fail use a trailing !, and methods that can't, don't. This way Option::unwrap() becomes Option::unwrap!() and doesn't have to have its name changed beyond that.

The problem is, of course, that a trailing ! is used for macros. So this does require changing the macro invocation syntax. Which is a shame, because using the trailing ! for macros has actually been one of the nicest syntax choices Rust made. For alternatives, we could go with e.g. #println(), but it just doesn't look as nice.

As for the ? syntax, I am sympathetic to the idea of making this easier to do, but I don't agree that this is the right way to do it. I think getting real monadic do notation is the desired end-goal here, and in the meantime, sticking with the try!() macro works just fine. Swift has the ? postifx operator largely because Objective-C sets a precedent for messaging nil that Swift wanted to have an equivalence for. Rust doesn't have the same design constraint.


That said, sticking with what's just proposed in this RFC, I think the modified-identifiers alternative is significantly better than making ! and ? into real postfix operators. But I'd rather go with my mini-proposal instead.


Given my mini-proposal, we can actually make it easy to provide both failing and non-failing variants for APIs that we expect many callers to want to unwrap. We can just provide an item decorator, e.g. #[synthesize_!], on an Option- or Return-returning method, that synthesizes a version of the method that has a trailing !, calls the original, and invokes unwrap!() on the result. This would require the original to return a literal Option or Result type (as the item decorator has to be able to figure out the return type of the unwrap!() call), but that would cover most cases.

Presumably rustdoc would be able to tell that the method was synthesized (either by not expanding the item decorator, or if that's not possible, having it attach a #[was_synthesized] attr to the new method) and modify the API listing to show just a single entry for the method with an indicator that it has an optional ! postfix. But that's just an aesthetic consideration and isn't required for this proposal to work.

This is, of course, entirely optional. We could instead just continue to have callers invoke .unwrap!(). And I don't know how many APIs it really makes sense to provide the dual methods for. I mainly suggested this because it's basically a way to re-create the identifier-modifying variant of this RFC on APIs where it makes sense, without having to modify the language for it.

@aturon
Copy link
Member Author

aturon commented Aug 21, 2014

I've been tied up with the Rust work-week, but I wanted to note that the plan is for ! and ? to work via traits, similarly to other overloadable operators. I will revise the RFC with these and a few other details, most likely after the work week wraps up.

I am sympathetic to the point that the sugar proposed here is a bit like a specialized version of Haskell's do notation. I don't want to get off on a tangent about whether do is a good fit for Rust, but in my opinion the sugar here is useful in its own right -- that Result is such a central part of programming with Rust that even do notation would be too heavy weight for working with it. It's also worth noting that neither of the operations being mentioned here correspond to purely monadic constructs:

  • The ! operator invokes task failure, a side-effecting computation
  • The ? operator returns from the entire function, which is not something that's possible with do notation (which would only support .and_then semantics)

So I feel that, even if we did later add do -- which seems unlikely in the first place -- the sugar here is still useful both by being even lighter weight, and by expressing not strictly monadic concepts.

@ben0x539
Copy link

(Note that Rust's io module is built with implicit map-like semantics: 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.)

I always found that more obfuscating than useful. rust-lang/rust#14409

Seems to go against the spirit of being forced to address possible error conditions as they happen, at least by explicitly using try!() or the proposed sugar.

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

@glaebhoerl
Copy link
Contributor

As foreshadowed: I don't have the psychic energy right now to render them into paragraphs, so I just threw my notes up as a gist, but here are my ideas about the Rustic exception handling system of the future.

(Will gladly respond with paragraphs if prompted by questions.)

@reem
Copy link

reem commented Aug 22, 2014

I think adding specific sugar for Option and Result in this way would be a mistake.

Last night, at the bay area rust meetup, we heard Niko speak about how far Rust has come in moving features out of the language. Giving Option and Result specific sugar is really just not needed - this problem can be better solved not with do notation, though that can help, but with better abstractions built on top of the Monad trait, when it lands. for loops with try! in them are not best fixed by replacing the try! with a trailing ? but by replacing the loop with Monad::sequence() and being done with it.

When those abstractions land, and I'm confident they will, with HKT this choice has a strong chance of becoming an obsolete and completely separate part of the syntax, which is really not something we want to introduce with 1.0 so nearby.

This is a significant step in the wrong direction - we would be encouraging things we don't even want to encourage (.unwrap(), primarily) by giving them convenient syntax in the language.

Even if we never get Monads or do or sequence, we would be adding a new, non-extensible, language feature to save 8 characters on ! (which we don't event want to encourage, and should probably rename to be longer) and 4 characters on ?, which is a tiny payoff.

A meet-in-the-middle approach of just allowing ? and ! in identifiers might be a nice way to make this more ergonomic in the short term.

EDIT/aside: @glaebhoerl I have a workaround version of something similar here: https://github.com/reem/rust-error that re-implements most of Any to allow for extensible errors.

@glaebhoerl
Copy link
Contributor

@reem As far as I can tell you seem to be re-using Any, not re-implementing it? :) My first thought was also to piggyback off of Any, but (a) we'd like to avoid having to allocate, which trait objects with Any forces us to do, and (b) we want the throwable errors to be fully listed and exhaustively matchable, catchable, handle-able. In other words the type system should yell at us if we throw an error we haven't declared, or if there's a type of error we've forgotten to handle, etc. The union types thing ((A|B|C), Either<A, B, C>, Any<A, B, C>, however we choose to write it) gives us both of these.

(As mentioned in the linked notes though you could do fn foo() throws Box<Any> or fn foo() throws Box<Error> or whatever if you wanted to, the two are orthogonal. They just go nicely together.)

@reem
Copy link

reem commented Aug 22, 2014

@glaebhoerl I just use Any as a bound because you can't use 'static as a bound for Self yet. I think this is actually unnecessary and I could remove it without breaking anything, because bounding Self to ErrorPrivate does the same thing. (Any doesn't provide any of the downcasting functionality)

I agree that both of your concerns are valid. I'd also to avoid allocating, which I think can be avoided with DST, but I'm not sure. A language feature for some kind of open enum would be nice, but my main concern is that it would make propagating errors between libraries hard - which was my main goal with rust-error.

Anyway, this is a thread hijack so we should probably discuss on IRC or elsewhere.

@ftxqxd
Copy link
Contributor

ftxqxd commented Aug 22, 2014

I really don’t see how allowing trailing ! and ? in identifiers would be a proper solution. In doing that, you’re forcing people to create two functions for every function that returns a result type. The fact that you’d consider making an attribute to do this for you just shows how hacky a solution that is. This RFC basically lets you do that but without the attribute (assuming there was an impl like impl<..I, T, E, R: ResultLike<T, E>, F: Fn<..I, R>> ResultLike<T, E> for F, so that foobar? and foobar?() would work, where foobar is a function).

I also still don’t see how monadic do would solve any of these problems. It’s really quite incompatible with imperative programming, and only supports one kind of monad at a time AFAIK. How would the following code:

fn frob(a: &[int], n: int) -> Option<Vec<int>> {
    let mut v = vec![];
    let mut x = 0u;
    loop {
        x += 1;
        if a.get?(x) == n {
            v.push(x);
        }
    }
    Some(v)
}

be expressed in do notation? (Yes, this code could be much better expressed in other ways. But sometimes, that’s not possible, e.g., a lot of parsers work with loops and mutable variables.)

@glaebhoerl I see you proposal as two main parts: union types (and matching on them), which I think is a great idea, and first-class exceptions, which I’m not so sure about. I think that making exceptions first-class in Rust is a bad idea, because it’s a lot less extensible than being able to use any type that implements a trait.

@glaebhoerl
Copy link
Contributor

@reem

A language feature for some kind of open enum would be nice, but my main concern is that it would make propagating errors between libraries hard - which was my main goal with rust-error.

If you want to hide the error type behind a trait object, you still can.

@P1start

first-class exceptions, which I’m not so sure about. I think that making exceptions first-class in Rust is a bad idea, because it’s a lot less extensible than being able to use any type that implements a trait.

...I don't understand how the second half of this is connected to the first half. Elaborate? (In particular: what is less extensible? And: "use any type that implements a trait" for what?) (I suspect you might be misunderstanding parts of what I wrote, but from this much, can't be sure.)

My observation is that with this special ! and ? syntax we're trying to awkwardly re-encode automatic exception propagation into the existing language with Results, and it would be better to admit that that's what we want, and just do it. It's a totally reasonable thing to want: many languages have it, and Haskell uses the Either monad (aka Error, Except) and the ExceptT transformer to add the capability to their imperative code (i.e., to automatically propagate errors in the monadic bind instead of having to explicitly pattern match Eithers at every step). We'd just be making that feature (quite literally the same thing) a built-in part of the language for convenience, which is also a totally reasonable thing to do. Analogously, Haskell uses the ST monad and STRefs when they want function-local mutable state delimited by lifetimes, but Rust chooses to have that feature built-in, which makes it waaay more convenient to work with.

@reem
Copy link

reem commented Aug 22, 2014

I think that giving a code example that says "this is weird code that doesn't do much" and using it as an example of why a feature is needed is strange. It's very unclear to me at a first pass what the failure condition of this code is, and it seems like that is actually a primary problem of encouraging this style of code - it makes the intention of code unclear when you have to re-invent combinators at every step of the way.

Basically, you're right, this code could be expressed in much better ways - and that applies to parsers too, Haskell's attoparsec is one of the fastest parsers out there and it's extremely easy to use because it relies on much higher order abstractions than what the above style relies on.

@ftxqxd
Copy link
Contributor

ftxqxd commented Aug 22, 2014

...I don't understand how the second half of this is connected to the first half. Elaborate? (In particular: what is less extensible? And: "use any type that implements a trait" for what?) (I suspect you might be misunderstanding parts of what I wrote, but from this much, can't be sure.)

What I meant is that with your system, everyone is ‘forced’ to use exceptions: they can’t make their own type that represents an error. I can’t make a weird new error type like enum MaybeResult<T, E> { Good(T), MaybeGood(T, E), Bad(E) }, and then use that as the return type for a function. (Well, I can, but it wouldn’t be ergonomic without this RFC.) With this RFC, I can do this (with a made-up name and method set for the ? trait):

// ErrorLike is the trait that allows for ?-ing and !-ing
// There’s probably some problem with how I’ve designed
// this trait, but it’s just for demonstration purposes anyway
impl<T, E> ErrorLike<T, E> for MaybeResult<T, E> {
    // Here an Ok result represents a successful ?, and an Err
    // result represents an early return from the function
    fn question_mark(self) -> Result<T, E> {
        match self {
            Good(v) => Ok(v),
            MaybeGood(v, _) => if random() { Ok(v) } else { Err(self) },
            Bad(_) => Err(self),
        }
    }
}

fn foo() -> MaybeResult<int, int> {
    MaybeGood(3, 1)
}

fn frob() -> MaybeResult<int, int> {
    let a = foo?(); // has a 50% chance of returning early
    let b = foo?();
    Good(a + b) // this has a 25% chance of being reached
}

This type would have a 50% chance of returning early when ?’d if it’s a MaybeGood.
(I understand that this type in particular is probably not very useful, but my point is that there might be some important use for something like this that we discover later on, but can’t use because we’d  be using exceptions instead, which AFAICT don’t support this.)

It’s not really a very strong argument, but it’s not supposed to be—I just think that things are a simpler without exceptions, and data types are created to emulate this instead. TBH, I wouldn’t mind first-class exceptions, but it just doesn’t seem very Rusty. (Anyway, I’m pretty sure that Rust has no exceptions by design…)


@reem Sorry, that was definitely a pointless example. Here’s a more realistic one:

fn parse(s: &str) -> Result<Vec<Expr>, ParseError> {
    let mut i = 0u;
    let mut exprs = vec![];
    while i < str.len() {
        exprs.push(match str[i] as char {
            '.' => parse_foobang?(str, &mut i),
            '$' => parse_variable?(str, &mut i),
            ...,
            a => return ParseError::unexpected(a),
        });
    }
    Ok(exprs)
}

This is partially inspired by some actual code I’ve written. The failure condition here is when one of the sub-parsers encounters an invalid string, or when an unexpected token is encountered.

Basically, you're right, this code could be expressed in much better ways - and that applies to parsers too, Haskell's attoparsec is one of the fastest parsers out there and it's extremely easy to use because it relies on much higher order abstractions than what the above style relies on.

I’m not really sure what you’re saying here. Are you saying that the solution to this problem is to use functional constructs everywhere? If so, I don’t really consider that a solution—Rust supports imperative-style programming for a reason. I don’t see why people can’t be free to code in whatever style they like.

@ben0x539
Copy link

@glaebhoerl You're intending a straightforward desugaring to today's Result<> style, yeah? Won't that mean that the return slots get really, really big really easily as people add just a handful of possible error values?

fn foo() -> T throws (U|V|W|...) is pretty much isomorph to fn foo() -> (Success<T>|U|V|W|...) with some secret struct Success<T>(T) that doesn't show up in the U|V|W|..., that seems a tiny bit flatter.

What specifically do you mean with "just like we've already done with ST and IO", out of curiosity?

Overall your approach sounds more interesting than "just" blessing try!{} by making it a postfix operator. Do we really need those full-on union types? Can't their semantics mostly disappear under throw being magical anyway?

@reem
Copy link

reem commented Aug 22, 2014

@P1start

I agree that we should be able to code in whatever style we want to - Rust should certainly be extensible in that way - I just think that adding ? and ! in this case is just unnecessarily pandering to a single kind of programming that can already be cleanly expressed with try! or try-for-option!.

The main point of my little rant was to just say that there are better ways to solve this problem than just cutting 4 characters off of a try! invocation. I think that it's not out of the question to add new syntax to deal with this sort of thing, but I think that a more general approach is needed.

For instance, I'm having a hard time imagining how either ! or ? could be encoded in general traits. Maybe that would help me get behind this proposal - if I could see an instance where this could be overloaded for other values to enable interesting things.

At present, I'm just not convinced that it's worth sealing ? and ! into the language for this use case when it could be easily solved with:

macro_rules! attempt (($e:expr) => match $e { Some(e) => e, None => return None })

fn parse(s: &str) -> Result<Vec<Expr>, ParseError> {
    let mut i = 0u;
    let mut exprs = vec![];
    while i < str.len() {
        exprs.push(match str[i] as char {
            '.' => attempt!(parse_foobang(str, &mut i)),
            '$' => attempt!(parse_variable(str, &mut i)),
            ...,
            a => return ParseError::unexpected(a),
        });
    }
    Ok(exprs)
}

In fact, try! could be easily generalized to work with both Result and Option, and could even be left open if it used a trait.

@ftxqxd
Copy link
Contributor

ftxqxd commented Aug 22, 2014

@reem The point of this proposal, AIUI, is to make Result and Option nice to use so that people use them instead of fail!. At the moment, a noisy macro (try!, which BTW takes up six extra characters (brackets!) :P) is used to make error-handling easier. However, it’s not amazingly elegant to use. This RFC makes it very elegant to use, meaning that hopefully people will use it everywhere they use fail! today. Yes, it’s not strictly necessary, but if we choose to ditch fail! (at least almost entirely), then we’ll need to make the alternative (using Result &c.) much easier.

@reem
Copy link

reem commented Aug 22, 2014

! under this proposal is fail! in sheep's clothing, which is a separate, but important issue to consider here. I think there is no reason to make failing functions easier to use or access, in fact I'd be in favor of renaming unwrap to something longer, like unwrap_or_fail to discourage its use.

Again, I'd be ok with ? if there was a good trait to make it overloadable - I just don't want Result and Option to be hardcoded as the only way to do things in the language - especially when this is really a slightly expanded version of Monad::bind.

@ftxqxd
Copy link
Contributor

ftxqxd commented Aug 22, 2014

@reem: @aturon has already confirmed that this will work through traits, and will revise the RFC soon. So don’t worry, this will be overloadable. 😄 (I also thought up a possible other use for such a trait—an impl for *-pointers (or at least an impl for wrapper structs around those pointers). It would fail on null pointers, so it’d be equivalent to Option<*{const,mut} T> but with a null-pointer optimisation.)

I’m not really so sure about ! either, but I think that its main use is when something can be asserted to work; that is, when a function being called is never going to return None/Err/whatever because some property is being obeyed (e.g., the .get() index not being out of bounds). That said, everyone makes mistakes, so I can see it being used when it shouldn’t be. I don’t really think that renaming unwrap to be longer would help, though. If anything, I wouldn’t mind unwrap being removed in favour of expect so that error messages become more informative.

@glaebhoerl
Copy link
Contributor

@ben0x539

You're intending a straightforward desugaring to today's Result<> style, yeah?

Not necessarily, though that might be one possible implementation strategy. It's isomorphic to returning Result, so you could do that. But you could also use e.g. unwinding.

The point is that:

fn foo() -> A throws B {
    // within this function:
    // expr `return blah` has type `!`, type of `blah` must match A
    // expr `throw asdf` has  type `!`, type of `asdf` must match B

    // When calling functions:
    bar().baz().quux()
    // the return type of one (`-> X`) is checked against the argument type of the next (of course)
    // return type of `quux()` must match A (of course)
    // any exception thrown by any of them short-circuits evaluation of `foo` and gets propagated
    // all of their `throws` clauses (exceptions they might throw) must match `B`
}

You could probably implement this behind the scenes by having them internally return a type like Result and desugaring every function call into the equivalent of match call_fn() { Ok(a) => a, Err(e) => return Err(e) }. But unwinding might be more efficient. I dunno.

Won't that mean that the return slots get really, really big really easily as people add just a handful of possible error values?

I don't see why. Both throw/throws and union types are extensions to the language. If you can keep return types from getting huge in today's language, then you can keep doing that. In particular, if a function returns -> Result<O, E> today, you can write -> O throws E and it'll work the same way, the only difference being whether the exception is returned to the caller or propagated by default.

What specifically do you mean with "just like we've already done with ST and IO", out of curiosity?

In Haskell, if you want function-local mutable state:

foo :: Int -> Int
foo n = runST $ do
    x <- newSTRef 1
    y <- newSTRef 2
    modifySTRef x (+n)
    modifySTRef y (*n)
    z <- readSTRef y
    modifySTRef x (+z)
    readSTRef x

(If we were to desugar this it would be a hornet's nest of chained >>=, a.k.a. and_then calls.)

In Rust, if you want function-local mutable state:

fn foo(n: int) -> int {
    let mut x = 1;
    let mut y = 2;
    x += n;
    y *= n;
    x += y;
    x
}

The s phantom type parameter in the ST monad directly corresponds to a lifetime parameter in Rust.

As for IO... well, it's just the fact that functions can do IO natively, instead of returning IO<T> values which you have to manipulate with map() and and_then() and so on and get executed by the runtime.

Returning Result<T, E> values and having to manipulate them with pattern matches and functions and propagate them by hand, or letting functions throw natively with the language doing the propagation, is the same kind of choice.

EDIT: Oh, I forgot to answer this:

Do we really need those full-on union types? Can't their semantics mostly disappear under throw being magical anyway?

I deliberately want them to be orthogonal features. The throws Foo and throw foo half of it is very nice, clean, well-behaved, precise, easily specified, theoretically pleasing etc., and throw is not any more magical than return. The union types half is considerably more magical (and has more hidden dragons, see "QUESTION"s in notes), but we can do without them and just use enums and trait objects as the exception types as outlined in the error interoperation RFC, if we're content with that. But I think the union types would probably be important as well for a truly seamless experience.

@glaebhoerl
Copy link
Contributor

Just to reiterate the orthogonality of these with one more example, borrowed from the std::io documentation. Currently:

fn file_product(p: &Path) -> IoResult<u32> {
    let mut f = File::open(p);
    let x1 = try!(f.read_le_u32());
    let x2 = try!(f.read_le_u32());
    Ok(x1 * x2)
}

With built-in exceptions as described:

fn file_product(p: &Path) -> u32 throws IoError {
    let mut f = File::open(p);
    f.read_le_u32() * f.read_le_u32()
}

If you have a consistent type for exceptions, like std::io does, this works perfectly without any need for union types. (This is probably what I should have started with!)

You only need union types if you want to be able to seamlessly propagate exceptions of different types from different callees inside the same caller, e.g. if some functions you call throw IoError and others throw ApiError and you want both of them to just pass through automatically. I was assuming this might be a common case, but I'm not actually sure.

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

@CloudiDust
Copy link
Contributor

@glaebhoerl I think the problem with the reintroduction of exceptions is mainly a cultural one. People will fight for the "one true way of error handling" all over again, no matter what the style guide says. The fact that "dealing with Result" and "dealing with Exceptions" are isomorphic doesn't seem enough to put out the flame.

That will be like Scala where there are multiple obvious ways to do the same thing. And the problem is that the ways are almost equivalent to each other.

And then, there are use cases for Rust where “exceptions implemented with unwinding" would not be appropriate, so it seems that we should go with the "desugar to Results" approach? Is this feasible in practice?

@glaebhoerl
Copy link
Contributor

I think the cultural problem with exceptions is that no language does it right, meaning: in a theoretically well-founded way, or at least no mainstream language (though I'm not familiar with Scala). This then leads to various kinds of accidental complexity and conflicting subjective interpretations about its meaning, purpose, and best practices, which then leads to the sense that the whole thing is a bad idea. This is similar to how people conclude that static type systems are a bad idea on the basis of Java and C++. Rust has been (quite rightly) steering clear of the whole area for basically this reason, because it's not clear what the right way to do it is, and if you don't know how to do it right, it's better not to try. Except that there is a good formulation, with a simple specification and a strong theoretical basis (i.e. it's completely well-behaved), and Haskell implements it as a library. So I think it would be kind of sweet if Rust were to adopt that approach at the language level.

Anyway, I think this is at the point now where it deserves its own RFC and discussion (my ideas have progressed somewhat since last writing). The main takeaway I wanted to leave here is just that I feel that the ideas in this RFC are a collection of workarounds for the lack of first-class exceptions, and that I think committing to them at this point would be short-sighted.

@CloudiDust
Copy link
Contributor

@glaebhoerl, Scala has a type Try[T] that turns the possible outcomes of an exception throwing function into one of the two variants Success[T] or Failure. Try is Scala's Result where the Err variant can only contain a Throwable. AFAIK you are proposing a try function with similar capabilities.(And its counterpart is try_err.)

The thing is that the last time I looked, the Scala people cannot agree whether they should encode the error in the type, or just use exceptions like in Java. Even though the two approaches are isomorphic.

@glaebhoerl
Copy link
Contributor

Thanks for the pointer. Based on this and this it appears that Scala has unchecked exceptions, i.e. exceptions are out-of-band and not tracked by the type system. So it's not remotely equivalent to returning Result<T, E> where both T and E are declared by the function: it's more like having every single function return Result<T, Box<Any>>. I'm not surprised that it's considered a mess! (Under the system I suggested, most functions would not throw exceptions (just as most functions today do not return Result), i.e. their signature is considered to be throws ! where ! is the uninhabited (bottom) type, which corresponds to returning Result<T, !>, which is isomorphic to T.)

So Scala is also, sadly, not an exception to the lineage of languages which do exceptions badly, and their experience doesn't let us conclude anything about doing them well.

@CloudiDust
Copy link
Contributor

@glaebhoerl, checked exceptions are considered one of the misfeatures in Java, and Java is the only mainstream language to have checked exceptions. Even other JVM languages don't have them, static-typed ones or not.

Honestly, I don't know if this is because "checked exceptions are a bad idea" or "Java's implementation of checked exceptions is a bad idea", but I am certain many people will be shocked when they find out that Rust has checked exceptions. :P

@netvl
Copy link

netvl commented Aug 24, 2014

Scala developer here.

Scala inherits its exceptions from Java because that's how JVM works; the only distinction is that in Scala they are unchecked (as they are in JVM in fact). However, using exceptions in Scala is discouraged, and in good codebases people usually use Option[T], Either[T, E] or E \/ T or Validation[E, T] (the last two are from scalaz library) and Try[T].

Option[T] and Either[E, T] / E \/ T / Validation[E, T] directly correspond to Option<T> and Result<T, E> in Rust. Try[T] is like Either[Throwable, T] except it provides convenient constructor which wraps code potentially throwing an exception:

val r: Try[ResultType] = Try {
  // code that throws exceptions
}
r match {
  case Success(r) => // r is computation result
  case Failure(e) => // e is Throwable object
}

All of these types are "monads" in Scala sense (they implement map, flatMap and withFilter methods, so they can be used in for comprehensions), and most of them are augmented by scalaz library so they really are monads (corresponding type class is implemented for them; it is possible because Scala generics are higher-kinded). Consequently, when people write error handling code, they usually use monadic interface and for comprehensions. Nothing really new here. Rust could do it too if it had HKT.

Hopefully this is relevant. As for RFC, personally I think that while the situation with error handling in Rust does need improvement, I don't know if this is the correct way, especially that invoking task failures becomes much easier.

I currently have two parser libraries and I have noticed that having only try macro which returns from the current function is not enough. I ended up having dozens of try-like macros which return from functions modifying corresponding return values and break from loops (from here and down); having syntax designed specifically for options/results will definitely be not enough for me, though it will be helpful in common situations. Maybe it is possible to generalize this kind of early returns/breaks?

@CloudiDust
Copy link
Contributor

@netvl, thanks for the info on Scala, I remember Akka as the famous exception to the "prefer monadic types to Java style exceptions" guideline and consider it an evidence that "guildlines are not enough". (Akka is just too big to ignore, being a component of the Typesafe stack.)

The fact that Scala's Either is unbiased also makes things harder. (Validation should have been in Scala core, and renamed Result, IMHO.)

@CloudiDust
Copy link
Contributor

@netvl, on a second thought, Akka is an actor library, throwing catchable exceptions into the supervisor is just like "throwing" task failures in Rust. So this may well be the the use case of exceptions.

And the fact that Rust task failures are unrecoverable locally helps steering people away from using them too much.

@vadimcn
Copy link
Contributor

vadimcn commented Aug 25, 2014

@P1start, regarding control flow structures precluding use of monads: I think F# computation expressions solve this quite nicely. Control structures can be desugared into functions that take closures for the controlled blocks of code, you just have to tell the compiler how to do it for the particular monad you are implementing.

@pczarn
Copy link

pczarn commented Aug 26, 2014

I would prefer a new foo!! syntax and leaving macro invocation syntax untouched.

@aturon
Copy link
Member Author

aturon commented Aug 27, 2014

It seems like the majority of the pushback here falls along two lines:

  • ! encourages unwrapping/failure, which we want to avoid
  • The proposed syntax feels overly tailored to the types in question, which we generally try to avoid in the language

I'm dubious about the first complaint, because contract checking is already happening, but it's currently invisible to the client in many cases because of the ergonomics of unwrap. Introducing ! should make it more feasible for contracts to be made explicit via Result.

That said, I'm going to break apart this RFC into two separate proposals -- the conventions, and the sugar.

That way, we can try to implement the conventions first, and see whether the resulting pain is enough to merit some syntactic support now, or whether we can tolerate waiting.

That said, we should strongly consider @pcwalton's RFC to free up ! for this use in the meantime.

Closing this RFC for now, conventions RFC to appear shortly.

@aturon aturon closed this Aug 27, 2014
@nielsle nielsle mentioned this pull request Aug 30, 2014
withoutboats pushed a commit to withoutboats/rfcs that referenced this pull request Jan 15, 2017
…version-badge

Feature/add crate io version badge
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.