-
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
Conversation
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 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.
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.
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).
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.
One alternative would be not doing the !
behaviour. :)
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.
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.
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 |
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.
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).
(Warning: enormous comment. I’m sorry. 😦) 👍 I really like this proposal—I believe that error-handling (with There is a recent trend towards removing uses of I especially like the symmetry between I also prefer the alternative for tying Regarding macros, I don’t particularly like the syntax proposed in the RFC. I’d actually prefer macros to be called with a 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 TL;DR: Great proposal. |
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 How about something like this as a replacement for
I acknowledge that that syntax would suggest the type
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 |
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. |
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
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
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.
This makes it seem both extremes are equally good or desirable. I also want to reiterate other's concern about Lastly, I don't agree with adding special syntax sugar to begin with, even if I agree that 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 |
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 I understand the concerns about 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
I agree that there’s no reason to restrict this to I don’t, however, agree that adding monadic 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 I also had an unrelated thought about |
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 :) |
My point is that the goal should be to make That said, it'd be great if this somehow extended to operator overloads, so you could just sprinkle |
Currently we can grep for |
I've long considered I think the general aim of making error handling and (The 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 |
`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 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.
I'm inclined to say that what we really should do is simply extend the identifier rules to allow for a trailing The problem is, of course, that a trailing As for the That said, sticking with what's just proposed in this RFC, I think the modified-identifiers alternative is significantly better than making 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. 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 This is, of course, entirely optional. We could instead just continue to have callers invoke |
I've been tied up with the Rust work-week, but I wanted to note that the plan is for I am sympathetic to the point that the sugar proposed here is a bit like a specialized version of Haskell's
So I feel that, even if we did later add |
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 |
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 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. . .
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.) |
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 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 ( Even if we never get Monads or 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 |
@reem As far as I can tell you seem to be re-using (As mentioned in the linked notes though you could do |
@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. |
I really don’t see how allowing trailing I also still don’t see how monadic 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 @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. |
If you want to hide the error type behind a trait object, you still can. @P1start
...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 |
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. |
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 // 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 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.
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. |
@glaebhoerl You're intending a straightforward desugaring to today's
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 |
@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 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 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, |
@reem The point of this proposal, AIUI, is to make |
Again, I'd be ok with |
@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 I’m not really so sure about |
Not necessarily, though that might be one possible implementation strategy. It's isomorphic to returning The point is that:
You could probably implement this behind the scenes by having them internally return a type like
I don't see why. Both
In Haskell, if you want function-local mutable state:
(If we were to desugar this it would be a hornet's nest of chained In Rust, if you want function-local mutable state:
The As for IO... well, it's just the fact that functions can do IO natively, instead of returning Returning EDIT: Oh, I forgot to answer this:
I deliberately want them to be orthogonal features. The |
Just to reiterate the orthogonality of these with one more example, borrowed from the
With built-in exceptions as described:
If you have a consistent type for exceptions, like 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 |
t1.bar(z).map(|t2| | ||
t2.bar)) | ||
``` | ||
|
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.
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 ))
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.
@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.
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.
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.
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.
@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
, #2
s 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.)
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.
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.
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.
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.
Perhaps this discussion should go to http://discuss.rust-lang.org/ . We seem to be diverging from the original RFC :-)
@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 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 |
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 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. |
@glaebhoerl, Scala has a type 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. |
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 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. |
@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 |
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
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 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 |
@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 |
@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. |
@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. |
I would prefer a new |
It seems like the majority of the pushback here falls along two lines:
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 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 Closing this RFC for now, conventions RFC to appear shortly. |
…version-badge Feature/add crate io version badge
This RFC sets out a vision for refining error handling in Rust, in two
parts:
fail!
andResult
Result
highly ergonomicIn a nutshell, the proposal is to isolate uses of
fail!
to an extremely smallset of well-known methods (and assertion violations), with all other kinds of
errors going through
Result
for maximal flexibility. Since the main downsideof taking this extreme position is ergonomics, the RFC also proposes notation
for consuming
Result
s:macro_name!
to@macro_name
.foo!
for today'sfoo.unwrap()
foo?
for today'stry!(foo)
While the two parts of this proposal reinforce each other, it's possible to
consider each of them separately.
Rendered