At this point the cognitive load required to read and understand Rust implementations of "typical" practical problems is rather higher than it is for C++. And it seems to be getting steadily worse from my perspective on the outside.
At this point the cognitive load required to read and understand Rust implementations of "typical" practical problems is rather higher than it is for C++. And it seems to be getting steadily worse from my perspective on the outside.
As someone that participated in that conversation, I think that's a pretty inaccurate characterization of it. It's not about when the writer learned the language, but rather, what problem you're trying to solve. If you'll allow me to summarize very briefly (perhaps at the expense of 100% accurary):
* Use unwrap/expect when you don't care.
* Use `try!`/`?` with Box<Error> in simple CLI applications.
* Use `try!`/`?` with a custom error type and From impls in libraries.
* Use combinators (e.g., map_err) when you need more explicit control.
You might imagine that you could use any number of these strategies depending on what you're trying to do, which might range from "a short script for personal use" to "production grade reliability."All of this stuff was available at Rust 1.0. (Except for `?`, which is today an alias to `try!`.) It all falls out of the same fundamental building blocks: an `Error` trait with appropriate `From` impls.
The one exception to this is that, recently, there has been a surge in use of crates like error-chain to cut down on the code you need to write for defining custom error types and their corresponding `From` impls. But it's still all built on the same fundamental building blocks.
->
"an `Error` trait with appropriate `From` impls."
Note, that it may be entirely necessary for us to invent new idioms to make progress in the art of programming.
They're all unique in what they do. I'd still say a simple procedural language like C is easiest to read, but 'modern c++' has more idioms and quirks than Rust, in my experience.
And Rust has the advantage that the ecosystem is very well tied together and it has a great community and documentation.
- "An Error trait" means that when you define a new type that will store error information, you have to define how it implements the Error interface.
- "appropriate From impls"... you are trying to wrap a number of error types in your own special error type, you need to tell the compiler how to convert another specific type into your type new type. There is an interface (trait) in the standard library for this purpose called "From". This is done as an alternative to inheritance in an error system. The trait signature looks like this:
trait From<T> {
fn from(T) -> Self;
}Why does every library implement its own Error type? It feels like reinventing the wheel, and it takes a lot of boilerplate code.
If you need to distinguish different kinds of errors, why not, for example, have a lot of useful pre-defined error types like Python does?
The benefit of lib-specific error types, though, is that they can be far more specific.
Now, in most cases, callers don't care at all about which specific error happened. They just want to print the error and be done with it. And that works just fine, because of the error handling machinery. But if you do want to drill down, then the option is there for you.
#[derive(Debug)]
enum ConfigError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for ConfigError {
fn from(err: io::Error) -> ConfigError {
ConfigError::Io(err)
}
}
impl From<ParseIntError> for ConfigError {
fn from(err: ParseIntError) -> ConfigError {
ConfigError::Parse(err)
}
}
fn read_config() -> Result<i32, ConfigError> {
Result::Ok(parse_int(read_config_file()?)?)
}
// given the following
fn parse_int(str: String) -> Result<i32, ParseIntError> { ... }
fn read_config_file() -> Result<String, io::Error> { ... }
Is, in a sense, the equivalent of this Java: int readConfig() throws IOException, ParseException {
return parseInt(readConfigFile());
}
// given the following
int parseInt(String str) throws ParseException { ... }
String readConfigFile() throws IOException { ... }
The reason i say that is that this: throws IOException, ParseException
Is essentially a sum type. It says that if this method results in a failure value, it can fail with one of two types of failure values. It might not look like a new type, because in Java, types are almost always nominal, and this is structural, but that's what it is. I think that throw and catch clauses are the only place that Java will let you define an ad-hoc sum type. You have to use polymorphism everywhere else you want a variety of types.Whereas in Rust, there are no structural sum types, and so the only way to make something resembling a sum type is:
enum ConfigError {
Io(io::Error),
Parse(ParseIntError),
}
Which means you also have to write the machinery to convert between the types.I wonder if it would help to have a compiler- or library-defined From impl for all newtype enum variants (or all newtype structs more generally), that makes the variant from its argument. Or maybe it could be derived. It would wipe out a lot of this boilerplate.
error_chain! {
foreign_links {
Io(io::Error);
Parse(ParseIntError);
}
}
and it'll even generate an aliased `Result` type as well as fancy chaining support.One takeaway I got from playing around with Rust (and using GitHub code search to work through my issues) was that coding style will probably differ considerably from project to project, which is very C/C++ like and maybe a good thing for the language, but I found a little disappointing.
That's been my criticism of Rust error handling. Rust's error handling system is very clever. It's logically sound. It manages to make functional programming and error handling play well together. But it's not user-friendly. For a while, it took far too much code to handle errors. So gimmicks were developed to make the necessary gyrations less verbose. These hide what's going on underneath. Thus the generations of error handling approaches.
Rust tried to avoid the complexity of exception handling, but ended up with something that's more complicated. Python programmers, who have a good exception system, notice this. In Python, you write the main case, and then you write an exception handler to deal with the error case. This works well in practice. Python has an exception class hierarchy. If you catch EnvironmentError, you get almost everything that can go wrong due to a cause external to the program. If you catch IOError, you get all I/O-related errors, including all the things that can go wrong in HTTP land.
With exceptions, if you're using some code that doesn't handle an error well, you can catch the problem at an outer level, get good information about the error, and recover. With error-value returns, after you've come through a few levels of function returns, you're usually down to "something went wrong". (Having written a web crawler, I've found this useful. A huge number of things can go wrong in HTTP, HTML parsing, SSL certificate handling, and the other manipulations needed to read a possibly-hostile web page. A crawler needs to catch all those and deal with them, deciding "try again now", "try again later", "log error and give up", or "try alternative access approach". This makes one appreciate a good exception mechanism.)
Exceptions have a bad reputation because Java and C++ implement them in ways that are inferior to Python's approach. There's no exception hierarchy. Knowing what exception something can raise is very important. Often, you don't.
Rust (and Go) are slowly backing into exception handling, as the panic/recover mechanisms acquire layers of gimmicks to make them more useful. Rust already has unwinding (destructors get run as a panic event moves outward), which is the hard part of exception handling. Thus, exceptions are more a religious issue than a technical issue.
Java has an exception hierarchy and exceptions as part of method signatures. If anything, Python's exception hierarchy started getting sorted out relatively recently.
Exceptions(and the runtime/memory costs they incur by pulling in RTTI)
ERRNO(on relevant *nix platforms)
Lifetimes tied to objects when things fail(this is a big one)
Plus any library-specific hackery(I've seen raw strings as errors before)
In contrast I've been writing Rust for ~1.5 years now and each library I've used is consistent and follows common patterns. Much like a lot of people don't grok functional until they understand the common patterns, so it is with Rust too.
Disable generation of information about every class with virtual functions for use by the C++ runtime type identification features (`dynamic_cast' and `typeid'). [...] Note that exception handling uses the same information, but it will generate it as needed.
[1]: https://gcc.gnu.org/onlinedocs/gcc-4.6.1/gcc/C_002b_002b-Dia...
Show me a compiler that's used in production which handles multi-inheritance exceptions without allocating extra data and I'll be happy to eat my words :).
Language != Implementation.
If the language would be "In GCC the thing X happens when ...", then ok.
No, they add extra control flow edges, which inhibit optimizations, affecting runtime performance. This is a lot of the reason why unwinding is optional in Rust.
I code in Rust and C++ every day, and am mostly equally experienced in both (maybe more Rust now, but this wasn't always the case). I disagree. Rust has some ergonomics issues that C++ does not, but the reverse is true too. Looking at rust from C++ you'll only see one and not the other, because you're used to the other.
(And as burntsushi said your characterization of the thread is inaccurate, people are suggesting things that are best for different use cases)
Rust hasn't really tried "hard" to make error handling simple. We have what we had during 1.0, and then we have the ? operator, which always existed as try!().
Other than the enforcement by the compiler, this is common in scala and f#, two other languages where raising errors is common.
Railway-oriented-programming is nice because it typechecks, and because it reduces cyclomatic complexity by short-circuiting without forcing you to handle the error until the end of the chain. Now, it is unfamiliar to those outside the FP community, but much of the rust language seems to be unfamiliar mixes of FP and low-level optimisation techniques.
With the Either type you don't even need to do that, but then errors on the left becomes convention instead of an enforced habit. The only odd part is the try macro. That should wrap the Val in a right, to preserve type information. I guess I could be convinced that Either = Error | Id [A].
No, ".unwrap" turns input errors into bugs, and there is no production code where this is more desirable than an exception.
1. If you're prototyping or writing a quick throwaway program, then unwrap will neither pick your pocket nor break your leg.
2. If you have an invariant that you either can't (or won't) move into the type system, then unwrap/expect/panic'ing can be appropriate precisely because if that unwrap gets tripped, then that will indicate a bug in your program that should be fixed.
The more succinct advice that I like to use is this: "if end users of your Rust application see a panic, then you have a bug." But this is slightly harder advice to follow because it's a statement about the user experience.
Absolutely but using the same mechanism (unwrap/panic) for both types of errors - recoverable and recoverable - can creates confusion. panic'ing for wrong user input for example as can be seen in example code.
And I understand that. Hence why unwrap everywhere is harmful.
> Absolutely but using the same mechanism (unwrap/panic) for both types of errors - recoverable and recoverable
Nobody is using panics with the intent to recover, in the classic sense of "recoverable error".
Parsing errors are recoverable as a class—you haven't irretrievably corrupted your process memory when you encounter one. Therefore, the parser itself should not panic(); it should just return an option type.
Parsing errors may very well be unrecoverable in a particular instance. The code calling the parser has every right to decide to unwrap() the option type such that a panic() will happen if the parse failed. The code calling the parser is likely business-logic code of an application, and is privy to knowledge like "if there is no configuration supplied here, then later code that tries to consume the configuration will have to crash" and so can decide to early-exit with a user-comprehensible error ("you don't have a config file!") rather than letting the later code crash with some weird error about a config value being Nothing.
.unwrap is only suitable for the unrecoverable case, while exceptions are suitable for both cases. Hence why .unwrap in examples is harmful imho.
For example, in my production Rust code, i deal with all errors in reading and parsing config files with .unwrap() or .expect(). If a program cannot read its config file at startup, it cannot correctly do its job, and so the only correct thing for it to do is to abort.
Have i misunderstood what you were trying to say?
Take for instance a web framework like Django that responds with a 500 error page with a stack trace when an exception occurs.
There are interesting problems to be solved in the space of error handling, e.g. error handling in asynchronous code. But Rust is not even matching the state of the art achieved by Lisp and Python decades ago.
unwrap()'s behaviour is essentially the same thing as an uncaught exception in Python, only you can actually check for where they occur in the code with a simple grep rather than hoping your test suite caught every possible failure case.