8 ms·
I was under the impression that the (somewhat) verbose syntax for error handling and memory management via the type system was a necessary side effect of Rusts
by dbattaglia 9y ago
I was under the impression that the (somewhat) verbose syntax for error handling and memory management via the type system was a necessary side effect of Rusts entire point of existence: a compiler-guaranteed safe systems language. Neither Python nor C force you in any way to pay attention to errors, making simple scripts much easier to write.
I guess I'm just surprised people think that Rust should be as simple to use as Python. Maybe I'm wrong.
- ajross 9y agoI think the complaint is more that Rust has seemingly tried very hard to make error handling "simple". But in the process it has managed to invent a whole series of new idioms and special syntax that is alien to pretty much everyone. There's a thread in /r/rust about this same article where you can look and see people suggesting all sorts of ways to write this that are split into clear sedimentary layers depending on when the writer learned the language. 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.
- burntsushi 9y ago> There's a thread in /r/rust about this same article where you can look and see people suggesting all sorts of ways to write this that are split into clear sedimentary layers depending on when the writer learned the language. 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.
- jackmott 9y ago"new idioms and special syntax that is alien to pretty much everyone" -> "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.
- Coding_Cat 9y agoI will say that as someone who was not a real expert at anything when Rust came out, but familiar with a lot, I find that learning c++ these days has the same problems, and so does learning a functional language. 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.
- kazagistar 9y agoIn case someone cares to understand what this means: - "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; }
- rspeer 9y agoSorry, I'm trying very hard to learn Rust, and I'm willing to accept a lot of its restrictions, but this didn't clear anything up. 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?
- jackcviers3 9y agoIt didn't invent any of these. This is railway-oriented-programming[1], using Either, leftMap and bind/flatMap, a conversion from Either to Try, with forced recover calls and explicit implicit conversions via typeclasses. 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]. [1]: https://fsharpforfunandprofit.com/rop/ https://fsharpforfunandprofit.com/rop/
- sidlls 9y agoMaybe I was doing C++ wrong all those years, but I find the cognitive load for Rust about equivalent. The error handling idioms of Rust exist, more or less, in well-composed "--no-exceptions" C++ code. The syntax might be a tad different, but the principles are more or less the same.
- Manishearth 9y ago> 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++. 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!().
- vvanders 9y agoAs someone who's been doing C++ for a close to few decades I's challenge you on that. In C++ you need to understand: 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.
- ahoka 9y agoExceptions don't need RTTI and have only runtime cost when thrown.
- cornstalks 9y agoExceptions actually can need RTTI, though not necessarily all of the RTTI that things like <typeinfo> provide. For some details, see the -fno-rtti flag for GCC, for which the documentation says[1]: 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-Dialect-Options.html https://gcc.gnu.org/onlinedocs/gcc-4.6.1/gcc/C_002b_002b-Dia...
- pjmlp 9y agoThat is a compiler specific implementation, the ANSI C++ standard doesn't require it.
- vvanders 9y agoThat's great and all but when every modern compiler does implement it that way it's not something you can ignore. 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 :).
- p0nce 9y agoThat and the bizarre thinking that ".unwrap" is a perfectly ok thing to write in some cases (don't worry it will never make it to production!). No, ".unwrap" turns input errors into bugs, and there is no production code where this is more desirable than an exception.
- burntsushi 9y agoOne of the hardest things about teaching error handling in Rust is that there is a lot of nuance. It's fun to say "unwrap is evil, don't use it," and more than that, it's not exactly wrong either. The nuanced version is that you shouldn't use unwrap for error handling, but: 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.
- p0nce 9y ago> 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. 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.
- kibwen 9y agopanics aren't for recoverable errors, in any circumstances. It is technically possible to "halt" the unwinding from a panic, but 1) that's intended for C interop, 2) the feature has been deliberately designed to make your life hell if you try to use it to emulate recoverable exceptions, and 3) Rust makes no guarantees that panics will unwind at all, it is entirely legal for a user to configure panics so that they all abort instead.
- ufo 9y agoFor reference, here is the link to the reddit thread: https://www.reddit.com/r/rust/comments/69i105/ https://www.reddit.com/r/rust/comments/69i105/
- Animats 9y agoI think the complaint is more that Rust has seemingly tried very hard to make error handling "simple". But in the process it has managed to invent a whole series of new idioms and special syntax that is alien to pretty much everyone. ... ways to write this that are split into clear sedimentary layers depending on when the writer learned the language. 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.
- pvg 9y ago
- rtpg 9y agoIn an abstract sense, it's possible to offer more functionality without requiring much more friction. This is the premise of languages with good type inference, relative to earlier versions of C++/Java. Though there might be more friction at some points, I imagine the Rust developers are taking these examples as good benchmarks for improvements. EDIT: This exercise is very similar to the frustration when starting to use Haskell. A lot of "simple" things feel more difficult because of the functional purity. But then you discover more patterns or libraries that help to handle this. Design patterns surely exist for Rust that have yet to be discovered, but will turn out to properly encapsulate a lot of the difficulty (when combined with language improvements)
- Sharlin 9y agoThere are many ways to have both enforced error handling AND less boilerplate. Java's checked exceptions are much maligned but would work very well here. Another way would be having more syntactic sugar for Result-style monadic error handling, like the do notation in Haskell or for..yield in Scala. Another issue raised by the original post is the fact that Rust has no top-level concrete error type that is convertible from all of the specific error types. This is also something that could be fixed without compromising other qualities of the Rust type system.
- burntsushi 9y ago> Another way would be having more syntactic sugar for Result-style monadic error handling [...] Another issue raised by the original post is the fact that Rust has no top-level concrete error type that is convertible from all of the specific error types. Well... Rust has both. Any type that satisfies the `Error` trait can be converted to `Box<Error>`, which is provided by the `impl<T: Error> Error for Box<T>` and `impl<E: Error> From<E> for Box<Error>` impls. And it so happens that this conversion can be done for you automatically using Rust's `?`. For example, this: use std::error::Error; use std::fs::File; use std::io::{self, Read}; fn main() { match example_explicit() { Ok(data) => println!("{}", data), Err(err) => println!("{}", err), } } fn example_explicit() -> Result<String, Box<Error>> { let file = match File::open("/foo/bar/baz") { Ok(file) => file, Err(err) => return Err(From::from(err)), }; let mut rdr = io::BufReader::new(file); let mut data = String::new(); if let Err(err) = rdr.read_to_string(&mut data) { return Err(From::from(err)); } Ok(data) } can be written as this without any fuss: use std::error::Error; use std::fs::File; use std::io::{self, Read}; fn main() { match example_sugar() { Ok(data) => println!("{}", data), Err(err) => println!("{}", err), } } fn example_sugar() -> Result<String, Box<Error>> { let file = File::open("/foo/bar/baz")?; let mut rdr = io::BufReader::new(file); let mut data = String::new(); rdr.read_to_string(&mut data)?; Ok(data) } The problem is that the conversion to `Box<Error>` winds up making it harder for callers to inspect the underlying error if they want to. This is why this approach doesn't work well in libraries, but for simple CLI applications, it's Just Fine.
- saurik 9y agoC does not force you to check errors, as everything that could fail must return an error code and you could simply forget to check it. Python checks the error for you, as anything that could fail throws an exception, and so you have to go out of your way to actively write code to manage to override that check (with a try/except) to ignore the error.
- gaius 9y agoa compiler-guaranteed safe systems language When I need that, I use OCaml.