On Error Handling in Rust
lucumr.pocoo.org
lucumr.pocoo.org
You're going to hit the same problem again with async, with transactions, and with resource management; indeed some of the stuff I've already seen about borrowing and the like seems achingly close to the same pattern. Introducing new sigils like ? for each use case is not going to be sustainable.
The nice way to solve this is higher kinded types, monads, and some kind of concise notation for composing them (e.g. Haskell's do or Scala's for/yield). Then you can do something like (Scala):
def read_value(host, port) = for{
sock ← TcpStream::connect(host, port)
parser = Parser::new(&mut sock as &mut Reader)
r ← parser.parse_value()
} yield r
and this is generic and extensible; you avoid blowing your syntax budget because the ← syntax is reusable for any "context" that behaves in the same way (which turns out to be most of them), and also for user-defined types. There's no need for macros or magic method names (If you want the FromError functionality you can use a typeclass). Everything's implemented with ordinary types and objects behaving in the ordinary way.Of course none of this is perfect - in Scala the ← syntax is "magic", implemented in the parser, and so the method names it invokes (map/flatMap) are also magic, and different languages have to fight over the best implementation for them. In Haskell the compiler knows about the Monad typeclass specifically, and the do notation is linked to that; if you were to write your own implementation of Monad, you wouldn't be able to use the syntax. There's plenty of room for innovation here, and I hope Rust eventually comes up with something better than either of those approaches. But an ad-hoc ? operator that calls a macro that calls a specially named method really isn't the way forward. I'm sure Rust can come up with something more generic and principled than this.
And unless I'm reading the RFC incorrectly, I think this is more principled than you're making it out to be. `FromError` is not magic or specially-handled by the compiler in any way, it's just a trait defined in the stdlib. The `try!` macro isn't calling any magic methods, it's just expanding to a pattern match that itself makes use of typeclasses. It's indeed true that this would only work with variants of the `Result` type, but I don't think that's especially heinous (users can easily supply their own specialized versions of this type, and almost always do). And if the `?` syntax is accepted, it will be able to be used with any type that implements the `Carrier` trait (which is sorta specially-treated by the compiler, though users can still override it via lang items), and would replace the `try! macro entirely. Nothing here is ad-hoc.
Finally, even if Rust had HKTs, I could be convinced that error handling in particular is important enough to require a dedicated syntax to set it apart.
FromError is not handled specially by the compiler, but it is handled specially by the try macro; does the signature of try make the relationship between the two more obvious? I haven't been following the state of Rust IDEs, but I'd hope that they can make it more obvious which typeclasses are involved than is clear from the text of the code (apologies for this awful sentence).
FWIW doing things this way doesn't rule out a dedicated syntax on top of it - see the recent SIP for async/await in scala.
> `Carrier` is still pretty parochial; it has this
> normal/exception distinction hardwired into it, no?
To reiterate my earlier point, I'm personally fine with the existence of an entirely separate mechanism for error handling. Mind you, not that this invalidates your desire for a more general mechanism for async et al. Until/if we get HKTs, we'll probably continue to achieve this with bespoke macros as per today's `try!`. > FromError is not handled specially by the compiler, but it is handled specially by
> the try macro
The `try!` macro is just as non-special as `FromError` (and `Result` and `Option`). You're free to recreate the whole ecosystem in your own libs if you'd like (ignoring the `Carrier` proposal for the moment and its associated syntax).Sure. I'm coming at this from a viewpoint of a) syntax is very important b) user-defined macros are generally undesirable
I'm not entirely sure it's just for this purpose. `do` used to mean something in Rust, but when that syntax was removed, the keyword just wasn't freed up. Of course, if we do gain HKT, it will be nice to have it, but I'm not sure that was the justification at the time.
Anyway, none of this particularly matters.
But I agree, in general, you absolutely want HKT for the reasons you mentioned.
Said another way, if HKT in rust is doable, let's do that - but if that turns out not to be the case, there are some nice conpromises such as this example, which I think, at least, is better than the proposed ? Operator, because it is a bit more general/versatile.
monad!(
sock <= TcpStream::connect(host, port);
let parser = Parser::new(&mut sock as &mut Reader);
parser.parse_value()
)
to TcpStream::connect(host, port).map(|sock| {
let parser = Parser::new(&mut sock as &mut Reader);
parser.parse_value()
})
but could you get that to compile to the same machine code as this? match TcpStream::connect(host, port) {
Ok(stream) => {
let parser = Parser::new(&mut sock as &mut Reader);
parser.parse_value()
},
Error(e) => { return e; }
}
Rust has the same "don't pay for what you don't use" mantra as C++, and introducing the overhead of stack closure in the canonical error handling method would be unacceptable. It might be possible to optimize monadic code as nicely as e.g. iterators, but I'm not convinced.Well not exactly because the second snippet is not equivalent to the first one (it doesn't wrap the result of parse_value() back into an Ok(), and thus I guess wouldn't compile), but aside from that yes: Result.map is trivially implemented in terms of match and (almost always) inlined: https://github.com/rust-lang/rust/blob/master/src/libcore/re... and the closure is itself inlined. So the resulting assembly should be the exact same between the two versions
Certainly I've been very impressed with the performance I've seen from Haskell (and Scala) on real-world problems, though I appreciate that's not quite the same thing, and a more realtime-suitable language that had the same level of expressiveness would definitely be a Good Thing.
It doesn't even rely on a smart compiler: https://github.com/rust-lang/rust/blob/master/src/libcore/re...
The #[inline] attribute is defined as a "standard (though very strong) inline hint" (there's also #[inline(never)] and #[inline(always)])
Going down one level and finding a better way to express the sugar around `map` and `flatmap` on Monads (and then `withFilter` in Scala) without resorting to compiler magic would be really cool and would let programmers add their own constructs that operate on a lot more than monads.
Out of curiosity, do you know of another language that has something on the level of Result type in its standard library? I haven't seen one. I know Scala has Either/Try, however those are both inferior to Result as a potential Exception replacement.
It's better named when it comes to being a value/error union: Result/Ok/Err is somewhat more obvious than Either/Right/Left, especially for people who don't make the connection between "right" and "correct".
Aside from that, Result was annotated with #[must_use], so the compiler will warn if you ignore a Result return value:
fn main() {
f1();
}
fn f1() -> Result<(), ()> {
Ok(())
}
=> > rustc test.rs
test.rs:2:5: 2:10 warning: unused result which must be used, #[warn(unused_must_use)] on by default
test.rs:2 f1();
^~~~~
doing that with Either would be weird.Those are not huge, but they're small tweaks specially making Result a better fit for an exception replacement.
I would argue that either is inferior because programmers empirically won't be bothered remembering that Left = Err and Right = Ok. Also fixing your Left to conform to a common error type which supports chaining is directly is also pretty important. This can be accomplished in a few different ways, but it's going to be done so often that Either ends up never being used for anything other than Results base class / Result is implemented as a type-class on either, with crappy names for error and ok.
Overally though I dislike Either, because to me, it is a symptom of a language lacking a way to declare anonymous type disjunctions inline[1], just like things like std::pair indicates a language lacks tuples.
1: Something like
def parseToken : ParseError | Int | String
looking at that maybe with inline type disjunctions the need for Result itself melts away.
This makes it very easy to tell whether code is handling potential failure conditions or ignoring them - you're either ignoring the return value (which is a compiler warning) or you're handling the failure condition. I'm reminded of Raymond Chen's post from 2005 about handling failure conditions, and recognizing code which handles all of its error conditions:
http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/35294...
I think this approach will pay great dividends in both understandability and readability.
In case my comment is taken as denigration, I'm saying this from a perspective of admiration. I think this is a fairly elegant way to solve a difficult problem, and learning Rust has been a true pleasure. I'm excited for the future of the language.
http://www.reddit.com/r/rust/comments/2jgck9/on_error_handli...
FILE* f = fopen("somefile", "r");
fwrite("aha", 4, 1, f); // <-- Unchecked use of f here
In Rust (pardon my rust, I'm totally ignorant), it'd be: let f = fopen("somefile", "r");
fwrite("aha", f)?;
The compiler would know that fopen may return an error, and forbid me from running unchecked code. Nice!I'd miss, however, the ability to handle all errors occurring from a segment of code in the same place, stuff that exceptions allow for. Pythonish example
try:
db = pgsql.connect(localhost)
stmt = db.prepare('INSERT INTO log VALUES(?,?)')
stmt.execute(('debug', 'Note to self: debug logs are noncritical'))
except Exception,e:
console.write('Could not write log to database %s' % (str(e)))
Sometimes error recovery is the same for all of the segment. I realize one could extract a function for the commonly recovered code, but this may lead to a too-many-small-functions-with-one-caller(tm) smell.Exceptions aren't the only way to achieve this. If Rust wants to keep return values as the error mechanism, perhaps it could find a way of allowing recovery to happen in the same place for a segment of code.
fn read_database() -> Result<Document, DatabaseError> {
let db = pgsql.connect(localhost)?
let stmt = db.prepare("INSERT INTO log VALUES(?,?)")?
stmt.execute(("debug", "Note to self: debug logs are noncritical"))?
}
So to have the handling code there as well, wrap it in an outer function (I have no idea if this is anywhere close to actual Rust): fn read_database() {
fn reader() -> Result<Document, DatabaseError>
let db = pgsql.connect(localhost)?
let stmt = db.prepare("INSERT INTO log VALUES(?,?)")?
stmt.execute(("debug", "Note to self: debug logs are noncritical"))?
}
match reader() {
Document(d) => d,
DatabaseError(err) => println!("Could not write log to database {}", err)
}
} try!({
let db = pgsql.connect(localhost)?
let stmt = db.prepare("INSERT INTO log VALUES(?,?)")?
stmt.execute(("debug", "Note to self: debug logs are noncritical"))?
} catch err: DatabaseError {
println!("Could not write log to database {}", err);
})
===> match (|| {
let db = pgsql.connect(localhost)?
let stmt = db.prepare("INSERT INTO log VALUES(?,?)")?
stmt.execute(("debug", "Note to self: debug logs are noncritical"))?
}) {
Document(d) => d,
DatabaseError(err) => println!("Could not write log to database {}", err)
}However, I imagine your type of solution could be used in a macro, like jeremyjh suggested in a parallel comment, producing simple code and the expected functionality.
In my opinion one of the biggest benefits of functions is the ability to name pieces of code. I've worked with a number of teams where a single line function with a single caller and a descriptive name is preferred to a line that needs a comment to say what it does.
match fwrite("aha", f) {
Err(e) => println!("Could not write log to database {}", e),
Ok(_) => { /* no problem */ }
} match { let f = fopen(); fwrite("aha", f); fclose(f); } {
Err(e) => println("either of fopen, fwrite or fclose failed"),
_ => ()
}
without something like a try! variant that breaks out of a block instead of returning from a function on error.By which you mean that errors as return values are bad, and that they are trying to sugar-coat the fact that they are using errors as return values?
Microsoft's upcoming M# language has also teased an interesting approach to error-handling. I'm very curious to see what they come up with.
http://lambda-the-ultimate.org/node/4862
http://joeduffyblog.com/2013/12/27/csharp-for-systems-progra...
Is a variant of C# targeted for systems programming, developed at Microsoft Research. It remains to be seen if it will ever be made public.
I did this in Haskell using MPTC, I have used the Convertible class like:
instance Convertible IOError RedisError where safeConvert = <conversion mapping here>
Then I have combinators like:
convEither :: Convertible e1 e2 => Either e1 a -> Either e2 a
and
convEitherT :: Convertible e1 e2 => m (Either e1 a) -> EitherT m e2 a
These are quite handy in close quarters combat. For example to open a file and bail on a generic IOException with a custom application exception, I can have something like:
instance Convertible IOException CustomError
getWords :: FilePath -> IO (Either CustomError [String])
getWords fileName = runEitherT $
do file <- tryIO (openFile fileName) >>= convEitherT
line <- readLine file
return (words line)Edit, also I think the "convertable" class idea is nearly expressible using associated types in rust today,but im not 100% certain about that
Maybe it's being pedantic, but I can't tell. Are Haskell's type classes strictly as flexible as multiple dispatch?
[1] https://github.com/glaebhoerl/rfcs/blob/trait-based-exceptio...
https://github.com/glaebhoerl/rfcs/blob/trait-based-exceptio...
Result is set up and marked to mandate its use, the compiler will complain if you ignore the result of a function returning a Result, not so for an Option. So idiomatically denoting a failure condition without a specific reason is Result<T, ()> not Option<T>: http://doc.rust-lang.org/std/result/#result-and-option.
Option<T> is for what it's name denotes, a value which may or may not be present (e.g. an optional field in a struct).
To elaborate on this slightly, it's not that Result is special cased by the compiler, there's a 'must_use' attribute which ensures that you get a warning if you ignore it, and the definition of Result has it.
http://doc.rust-lang.org/core/result/enum.Result.html#method... http://doc.rust-lang.org/core/option/enum.Option.html#method...
Either or both of these would be very appealing.
I remember asking on IRC a few times to see if I could get some interest. One of the things I proposed was a postfix operator in place of try, so I'm pleased to see something like that appear.
For some reason it just really bothered me to see all of my statements begin with "try!" rather than what they are actually intended to do. The "?" could be a great help.
I mostly do not care about the type of error condition and handle the error somewhere really far up the stack (log or messagebox) and provide the ability for a retry. Can i do something like a general try/catch far up the stack? in my opinion, this neatly prevents code from beeing littered with error handling which i very much prefer.
Rust does not have exceptions, so not exactly. You _could_ spin up a new task (thread) with the code, and then monitor that task for failure.
One thing that I don't see here but which I think it might be important is to grab the stack trace in the failure, or atleast the Filename / Line Number of where each Err is allocated. It will be really useful to be able to see the details for figuring out what went wrong, and following the stack trace is very important. With that I think the abstraction is 99%+ superior to exceptions, which is quite a step forward for the programming craft.
What really has to be guarded against though is that a lot of functions are going either have function signature that is much more verbose which leads programmers `cheating` and just `swallowing` the error by forcing the result out -OR- Return type inference might save the day (not sure if Rust has return type inference of not), however it doesn't solve everything, writing the return types on public functions pretty quickly becomes standard operating procedure, and then programmers are going to get lazy again and we are right back at checked exceptions `polluting` their interfaces and they might not want to deal with it.
interface Perfect {
def foo : Foo
def bar : Bar
}
interface NotPerfect {
def foo : Result[Foo, FooErr]
def bar : Result[Bar, BarErr]
}
Which of these do I implement first? Does the type system allow me to substitute a Perfect when a NotPerfect is required, or do I have to re-teach it every single time, that in fact, it's totally ok to do so (make a method `perfectCanBeUsedForNotPerfect(p : Perfect) : NotPerfect = new NotPerfect ....)`? Can I fix the issue for a limited set of cases and declare that Perfect implements NotPerfect and is therefore a subtype? Can I do similarly if I implement NotPerfect after Perfect was implemented (add an implicit conversion from Perfect to NotPerfect)? Can I just avoid the whole problem all-together with by-name subtyping (aka Point3{x,y,z} is considered a subtype of Point2{x,y} because Point3 has all the same named fields as Point2 with an extra one added) and informing the type system of the fact that Foo is a valid substitution for Result[Foo,_] because Result[A,B <: Err] is a type-disjunction of A | B and type-disjunction allows for substitution by the disjoint types? Where is Rust going to fall on this continuum and how many programmer hours is going to waste for each and every one of them to teach the its type-system over and over and over these simple patterns via boilerplate code?We static programmers will grumble on regardless, we pay our dues to the type checker because we believe its gives us structure and forces failure when while we write the code, not because we want to write boilerplate to teach it simple facts.
...
All in all this is really cool. From a Scala perspective it's a much improved version of Either (and now Try), and it's going to be supported by the standard library in Rust. Which is awesome.
Either itself basically is Scala saying, we don't have type disjuction, so here is a disjunction of 2 types which we will call Left and Right. And by convention Left is used to hold the error condition.
Because no one could remember that Left by convention meant error, and because it would by nice to gussy up what is held in the error type by having your errors be able to point a source error, Scala then later added a Try disjunction. Which is composed of two types Success[A](a:A) and Failure(e : Throwable).
The Success part is fine it's hard to mess that part up, it's just like Ok, having a uniform this just a wrapper around another type would be nice but, we suck that down as programmer business as usual, the Failure side however leaves a lot to be desired, by DESIGN the failure side cannot hold a failure and instead has to hold an exception, this is wrong, failure is a failure. Given the runtime on which Scala runs, the JVM, ignoring exceptions all-together is probably a worse evil. However it should have been made a special subcase of the Failure subclass rather than the only thing it can contain, or Try itself should have been the sub-implemenation of some more generic structure.
What to add a custom message to your failure? Ok make an exception that has a message in it.
Want to pass a Failure up the chain while adding your own error message for context? Ok make an exception then add the failure into it.
Want to fail without an exception? Ok make an exception and put it in the Failure.
Looks like Rust is moving towards a great implementation. This is something that has to be done in the standard library and has to be done well. The more I see of Rust as a language the more impressed I am, when is Rust going to compile to Javascript, oh never I see[1], well back to my ScalaJS cave then :)
> not sure if Rust has return type inference of not
Rust has return type inference for function invocations, but not for function signatures. This is a conscious decision to force all functions to provide an explicit contract rather than having any global type inference. And really, I wouldn't want function return types to be completely inferred in Rust given its last-semicolon rule.However, there is an RFC that would allow for "abstract return types", which would let a function state that its return type implements a certain trait and allow the programmer to omit the concrete type. This is somewhat essential for the new closure design, and also greatly benefits functions that return iterators (but it might not be relevant to your question at all, I haven't had my tea yet...).
https://github.com/rust-lang/rfcs/pull/105/files
> when is Rust going to compile to Javascript
Never officially, but that doesn't mean an Emscripten backend isn't possible https://github.com/rust-lang/rust/issues/2235 :)Part of me wonders if the best way to do it wouldn't be to to start at Rust 1.0 and go on a tour backwards through history, i.e. "here's a great language, now let's all make terrible decisions to make it worse!" :)
Real quick for those that don't understand, when I say Type Disjunctions (aka union types) I mean a type which has a value which the type system is guaranteeing is 1 of X different types. So Either[String,Int] is a specialized case where X=2 and the value is either a String or an Int. This ends up being really similar to Tuples, Tuples are often given their own syntax, typically, (A,B,...) and frequently in languages that don't support the tuple abstraction natively, things like std::pair<A,B> pop up as a poor substitute special cased to X=2 element tuples.
This leads to my view that Either is to Type Disjunctions as something like std::pair is to Tuple, scratching the surface of a deeper and much richer abstraction. When it pops up in your language its a symptom of lacking type unions, just as std::pair is a symptom of lacking tuples.
Do you think Rust will ever have native support for type disjunctions?
I understand that it probably principly uses object hierarchys for this type of stuff, and in a lot of ways that makes a lot of sense, but often time when writing statically typed message passing code, aka Consumers / Producers, the more ad hoc representation + syntax support for unions eliminates huge traunches of boilerplate and provides static guarantees that all the cases are handled consider this code for example.
As real world, recurring pain in the neck example, in Scala to preserve type safety and exhaustiveness checking at my company we create a sealed trait hierarchy where each subclass wraps each different type in the disjunction.
It's a poor man's type disjunction and it takes A LOT of boilerplate.
Its still worth it to jump through these hoops because in a message passing system built around producers/consumers(aka, sources/sinks, emitters/handlers etc) we get the following important benefits:
a. Only messages that fall in a set of allowed types can be passed into a handler, guaranteeing that a message the handler can't handle won't be passed to it.
b. A handler is typically backed by a match expression which handles the different subtypes the handler handles. We used a sealed trait in scala pattern which guarantees that if someone adds another type to the hierarchy without adding it to all the handlers (which might be in different codebases) there is a compile error for a non-exhaustive match in the handler's implementation instead of a runtime blowup there when the unhandled type falls through the match.
Obviously this is just scratching the surface of the benefits that type disjunction can provide. Hopefully enough programmers and language designers will have an 'aha moment' and realize that just as adding lambda's to abbreviate anonymous functions opens up the world of higher order functions, and tuples just eliminate doing the same thing thousands of crappy different ways, and having an Option (aka Maybe) type eliminates null pointer exceptions, and hopefully standard library sanctioned Result type eliminates unhandled exceptions, having language support for type disjunctions will have benefits similar to all these other `essential` features.
TL;DR Type Disjunctions are really useful, and are missing fundamental in most type systems do you think Rust will support them?
Result is simply defined as
pub enum Result<T, E> {
Ok(T),
Err(E),
}
where enum is a general 'variant type' mechanism.(Nor is Rust a fan of type hierarchies in general - it doesn't even have subclassing.)
Does it allow inline type disjunction declarations?
Rather than (never written rust before) something like:
pub enum ParseResult {
ParseError(Err)
IntResult(Int)
StringResult(String)
}
def parse : ParseResult
Allowing something like this newtype ParseError = Err
def parse : ParseError | Int | String = ...
This avoids having to create specific names for each different type in the disjunction.I mean we could implement tuples like this
pub tuple MapEntry<K,V> {
Key(K)
Value(V)
}
class SortedMap<K,V> {
def firstEntry() : Option<MapEntry<K,V>>
}
but everyone probably agrees, that we can figure out that key's are first and values are second, so let's just do this: class Map<K,V> {
def firstEntry() : Option<(K,V)>
} match foo {
ParseError(e) => ...
IntResult(i) => ...
StringResult(s) => ...
}
you need something adorning the left to determine what 'e', 'i', and 's' are; you could use the type, but compared to that it doesn't save much typing to just name the branches (which can always be abbreviated), which avoids issues with multiple variants that happen to have the same type.The Boilerplate grows really fast as you try to pass results up a call hierachy.
So let me extend the example to demonstrate it how it doesn't scale:
//Sorry for the Scala-ness
//Presume a mapByType partial function on all discriminated unions if the union value is of that type, then it calls
//the partial function otherwise it just returns whatever it's current value is
def lexAndParse : ParseError | LexError | Int | String = lex().mapByType{ case t : Token => parse(t) }
newtype LexError = Err
def lex() : LexError | Token = ...
newtype ParseError = Err
def parse(t : Token) : ParseError | Int | String = {
tryParseInt(t).orElse(tryParseString(t)).getOrElse(ParseError("$t not Int Or String"))) }
}
def tryParseInt(token : Token) : Option[Int] = ...
def tryParseString(token : Token) : Option[String] = ...
versus: pub enum LexParseResult {
LexError(Err)
ParseError(Err)
IntResult(Int)
StringResult(String)
}
def lexAndParse : LexAndParseResult = {
match lex() {
LexResult.LexError(e) => LexAndParseResult.LexError(e)
LexResult.TokenResult(t) => match parse(t) {
ParseResult.ParseError(e) => LexParseResult.ParseError(e)
ParseResult.IntResult(e) => LexParseResult.IntResult(e)
ParseResult.StringResult(e) => LexParseResult.StringResult(e)
}
}
}
pub enum LexResult {
LexError(Err)
TokenResult(Token)
}
def lex : LexResult
pub enum ParseResult {
ParseError(Err)
IntResult(Int)
StringResult(String)
}
def parse(token : Token) : ParseResult = {
match lex(){
Token(t) =>
tryParseInt(t).map(r=>ParseResult.IntResult(r)).orElse(tryParseString.map(r=>ParseResult.StringResult(r) )).getOrElse(ParseResult.ParseError("$t not Int Or String"))) }
LexError(e) => ParseResult.LexError(e)
}
}
def tryParseInt(token : Token) : Option[Int] = ...
def tryParseString(token : Token) : Option[String] = ...1. wrapper variants (where the choosen instance is given a key or it's own wrapper type)
2. ordered variants (where the choosen instance if keyed by it's position)
3. type variants (The only way to differentiate is by type, duplicate types collapse into one type)
I obviously prefer type variants where:
Int | Int | Int simplifies to Int
I think it gives the typechecker the ability to make and understand a much larger variety of useful properties.
For example a method that takes
def print(x: Num | String)
it will accept
Int | String
or String | Int
or String | Double
or String
or Int
or Doubleobviously that isn't totally ideal, you probably rather use polymorphism. (I think I read that the Ceylon implementors thought it would be a big win here then they preferred to use polymorphism, but I could be spreading FUD against my own postion :)
BTW I would argue that type variants have the opposite property of tuples in that as they grow longer that make code much more comprehensible.