Hacker News new | past | comments | ask | show | jobs | submit login
My Struggles with Rust (compileandrun.com)
353 points by wkornewald on May 7, 2017 | hide | past | web | favorite | 318 comments



One thing I've found with rust is that you struggle struggle struggle trying to do a simple task, and then finally someone says "Oh, all you need to do is this".

Rust has already reached the point where it leaves the world behind. Only the people who have been there since the early days really understand it, and getting into rust gets harder and harder as time goes on.

Yes, there's some awesome documentation, and the error messaging has gotten a lot better. But the power and flexibility of Rust comes at the cost of it becoming harder and harder to figure out how all the thousands of little pieces are supposed to fit together. What's really needed is a kind of "cookbook" documentation that has things like "How to read a text file with proper error handling" and "what is the proper way to pass certain kinds of data around and why".

Right now there's a lot of "what" documentation going around, but little that discusses the "how to and why".


I don't know if that's quite true. While Rust is very deep, I've found that you can get quite far with a few crucial pieces of info (note that I don't speak about generics or macros; haven't worked a lot with them):

* All variables are expected to be sized. So, learn what's sized and what isn't.

* Understand traits and how they add functionality to types. Have a small dictionary of common ones (From, Into, Debug, etc.)

* Learn how to write blanket implementations for traits. This can make your code lighter-weight. You also learn to start looking for blanket implementations.

* Encapsulate ownership details when possible. I'm not sure about the best way to explain this, but...at a high level it means "structure your types so that you avoid sharing ownership".


Any links or ideas on how to get started with some of these? Understanding traits and knowing common ones is as trivial as reading the Rust Book, but ideas like "write blanket implementations" and "encapsulate ownership details" are less easy to find.


I really started looking into blanket implementations after I saw this example [1] in Tokio. Basically it says "implement the `AsyncRead` trait for any boxed type that implements `AsyncRead` or any `AsyncRead` trait object. The Error trait contains more examples [2].

As for "encapsulate ownership details". Hard to describe, but...let me point to Tokio [3] again. In Java if you wanted to share an IO reader/writer with two different objects you'd use the same type and pass the same reference to both owners. In Rust, the same idiom would require the wordy use of `Arc<Mutex<Type>>` or `Rc<RefCell<Type>>`. I hope we can both agree that that's frustrating. An alternative (as demonstrated by Tokio's `split` method) is to separate your base type into two other types - reader and writer - that can be owned independently. No more externally-visible ref counting. Of course, this isn't possible in all scenarios: there absolutely are times when you don't have a clean separation of types and have to ref count; but, avoid it if you can.

I learned all of this by poking into libs, asking a ton on the IRC channels (everyone is super friendly) and failing a lot. Any errors are my own. Hope that helps!

[1] https://github.com/tokio-rs/tokio-io/blob/master/src/lib.rs#...

[2] https://doc.rust-lang.org/src/std/error.rs.html#281

[3] https://github.com/tokio-rs/tokio-io/blob/master/src/lib.rs#...


> but little that discusses the "how to and why"

Have you read the book's chapter on error handling?[1] It's being replaced in the second version of the book, but I still plan on maintaining it as a blog post[2]. Any advice you might have to add more of what you want would be helpful. (And I ask this because I tried to attack the "how to and why" angle, so I'm wondering if I got that wrong.)

[1] - https://doc.rust-lang.org/stable/book/error-handling.html

[2] - http://blog.burntsushi.net/rust-error-handling/


> Rust has already reached the point where it leaves the world behind. Only the people who have been there since the early days really understand it

This was my feeling when I got into Ruby on Rails (years too late).


I got into Python/Django seriously in 2013, which is a fair way along in their history, and have loved it ever since. Now even doing async & websockets stuff using django-channels after thinking that would never be possible.

Wonder why the difference?


These are the exact same complaints of students learning to program for the first time.

Have you tried functional programmng? Lisp? Ocaml? The complaints of newbie functional programmers are also nearly the same.

The "struggle" is necessary. If there is no struggle, there is no learning of fundamentally new approaches you are not yet comfortable with.

See it as part of the training regimen that lets people emerge as stronger programmers on the other side.


No, the training material for something like Python is orders of magnitude easier to get into than things like OCaml, Haskell, and Lisp. Python has doc and many many books taking you from A through Z. Many of these other languages have "A" and then pick back up at "S", but skip over "B"-"R". You basically get a feel for syntax, control flow, and a few other topics, but don't get the meat of why that language is different. Not everyone wants to learn by digging into open source projects.


If it is something that OCaml, Haskell and Lisp are not lacking, it is books written about them. We can argue on technical points. But the existence of documentation is a non-issue for all of these languages.

https://ocaml.org/learn/books.html

https://wiki.haskell.org/Books

http://lisp-lang.org/books/

It could well be, that these books are written for experienced programmers who don't need basic concepts (control flow, object orientation, etc.) explained. But that was not the point.

The absence of an easy road for these langauges is because they need to teach some powerful abstractions first that have no equivalence in Python, Java or C.


There are books yes and I've read some of them mostly cover to cover (1 Haskell, 1 F# and 3 common lisp) and still can't proficiently use any of those. Yes there are some things you have to learn like macros, currying, monads. I fully agree with you on that, but feel the task could be much simpler with better intro material. Perhaps the shear amount of material failing to work for so many people is a 100% indication that you're correct and the hurdle can't be made easier to overcome...I don't know. Slightly off-topic, if you're correct...do you ever think FP will take off with the masses?


There is an active (but just started) project to write a cookbook: https://blog.rust-lang.org/2017/05/05/libz-blitz.html


Check out the rust libz blitz, a documentatuon overhaul initiaitive by the rust devs. It addresses all of these issues.


(someone recently told me that they were working on just that -- a cookbook -- for common rust tasks. Let's see!)


I think it is 100% the case that the best way to learn Rust is to be on the IRC channel (#rust or #rust-beginners, either is fine for beginners but the latter is unlogged and more focused if you prefer) and ask lots of questions and also lurk. I'm not sure if that's good in absolute terms for the language, but it's very helpful.

(Which probably means your first project should be something open-source so you can share your code easily)


Personally, I learned Rust with "The Book" and "Rust by example", I'm still learning it!

My time never really allows me to be on IRC regularly. Point being that if you can't get on IRC, I don't think it's necessary.

And honestly, error handling was the most difficult thing to understand, until you start groking why sized types are required as return types from functions and that every function call up the stack requires its callees to compensate for all it's error possibilities.

error_chain! has made this all so much better.


How is this problem solved for C++?


The nice thing about C++ is that it is so widely used that for just about any task you can find examples of how to do it (usually many different ways to do something), especially for tasks like this article. You will also find plenty of C example, which you could also use in C++.


Unfortunately 90% of them will be outdated/wrong/unsafe. Being backward compatible with 30 years of code means you're unlikely to find the right answer for the present. And worse, you won't know it.


I think you are spouting a bit of hyperbole. Some might be unsafe but there are more good examples out there than you are willing to admit. And many use modern C++ and demonstrate smart techniques.

If you are not being hyperbolic then please backup your 90% claim.


You base your 90% claim based on what? I have entirely different experience and I am using C++14 and C. K&R "C programming language" is from 1978 and it's still one of the best books about C still used in 2017. I see many stackoverflow answers updated from C++98 to C++11/14 and some even to C++17.


As someone whose first introduction to C was through K&R, and still has that torn and tattered copy on his bookshelf, the idea that we're still recommending K&R as "one of the best books about C" only validates arcticbull's (sadly-downvoted) point.


K&R is so clearly written that I was able to understand it as a sixth grader. (At least, I felt like I did; subsequent events later proved that I did not understand pointers, but I don't think it was K&R's fault that a sixth grader didn't understand.) That's why it's still being recommended and the much thicker tomes also purporting to teach the C language have been forgotten. It's probably the same reason that your copy is torn and tattered.


> It's probably the same reason that your copy is torn and tattered.

Er, well I was also using it as a mousepad for about three years...


I don't share your opinion and most people I know that do C do not share it also. It's good base to get familiar with language, no one say you can't expand your knowledge further after reading this book.

I've read some other "modern" books about C like "21st century C" but they are not for people that do not know C and they are not so good for people that know it.

Can you provide any arguments to support what you wrote? What alternative you would propose for learning C?

https://www.amazon.com/Programming-Language-Brian-W-Kernigha...

Read what majority of opinions say. It's really easy to criticize without giving better alternative.


Keep in mind that I and the parent were mostly discussing C++, a very different language from C.


Sadly by what also helped it gain adoption but also hinders the language's safety, copy-paste compatibility with C (to a certain extent).

Meaning that many C++ applications, even nowadays, are actually C compiled with C++ compiler (not even classes are used).

So the transition between skill levels is quite gradual.

For me it is hard to tell, because I know the language since the C++ARM days, which means it had a feature size similar to Object Pascal and actually smaller than Ada.

Back then compiler writers even though it was easier to implement Ada compilers than C++ ones.

So slowly we got used to the new features, being discussed in books or programming magazines.


This is exactly my experience. My work's internal code which was written in the early 90's basically agrees with everything above. It is C code with a few extra keywords ("new" "delete" and "static_cast"). When I see modern C++ code it looks completely alien to me.


It changes slightly more slowly (written reference books aren't immediately out of date) and there are far more practitioners answering stack overflow questions :-).


Hello Justin Turpin! Sorry to hear your struggles with rust. It's always going to be a bit more verbose using rust than Python due to type information, but I think there are some things we could do to simplify your code. Would you be comfortable posting the 20 line code for us to review? I didn't see a link in your post.

Anyway, so some things that could make your script easier:

* for simple scripts I tend to use the `.expect` method if I plan on killing the program if there is an error. It's just like unwrap, but it will print out a custom error message. So you could write something like this to get a file:

    let mut file = File::open("conf.json")
        .expect("could not open file");
(Aside: I never liked the method name `expect` for this, but is too late to do anything about that now).

* next, you don't have to create a struct for serde if you don't want to. serde_derive is definitely cool and magical, but it can be too magical for one off scripts. Instead you could use serde_jaon::Value [0], which is roughly equivalent to when python's json parser would produce. * next, serde_json has a function called from from_reader [1], which you can use to parse directly from a `Read` type. So combined with Value you would get:

    let config: Value = serde::from_reader(file)
        .expect("config has invalid json");
* Next you could get the config values out with some methods on Value:

    let jenkins_server = config.get("jenkins_server")
        .expect("jenkins_server key not in config")
        .as_str()
        .expect("jenkins_server key is not a string");
There might be some other things we could simplify. Just let us know how to help.

[0]: https://docs.serde.rs/serde_json/enum.Value.html

[1] https://docs.serde.rs/serde_json/de/fn.from_reader.html


While it's too late to change the name "expect", could one create an alias for it and call it say, "on_error"?


I'd expect 'on_error' to take a function as a callback, not a string. But yes, a better named function could be added


Perhaps or_die, similar to Perl?


Yep, this is what I always wanted it to be! But maybe or_panic would be better. Naming things is hard.


or_fail_with("") ?


expect[ing_error]("could not open the file")


You're not expecting an error. On the contrary, you're expecting a value or throwing an error.


You're expecting specific errors.


In my Rust crates, I use a custom trait[1], with implementations for Option and Result, that provides an expected() method which, if it panics, prefixes the message with "expected: ". So you can write

  foo().pop().expected("foo length >= 1");
and if it fails, the error is something like "panicked at 'expected foo length >= 1'".

[1]https://gist.github.com/edmccard/8898dd397eec0ff3595c28ada52...


A failed expectation for one user isn't always an error for other users; calling out on_error may confuse people because it indicates an error may occur and must be handled.


Or `unwrap_msg()`?


If I know that the unwrap is 100% safe I tend to write: `.expect("Invariant: $REASON_WHY_THIS_NEVER_FAILS")`


Cool trick!


It might be easier to do:

  let config: HashMap<String, String> = serde_json::from_reader(file)
      .expect("config has invalid json");
This means that you can just do

  let jenkins_server = config.get("jenkins_server")
      .expect("jenkins_server key not in config");


Was the expect method​ supposed to be named except, as in exception? That would make a lot more sense.


If I recall correctly we just couldn't come up with a great name for it. To me "expect" is a positive action, but the argument is about it failing to meet the expectation. Semantically I like `thing.unwrap_or(|| panic!("failure message"))`. It feels more like what I would want to say, but it just is so wordy.

Ultimately I'm happy we just picked something and moved on, but still mildly annoys me whenever I write it. If only we found that perfect method name way back when...


  .beware("could not open file");


    .be_wary_of("but hole")


The java optional api uses orElseThrow which I think is quite clear.


Agreed, I've never liked 'expect' either; 'or_else_panic(msg)' would be much clearer

Edit: 'or_panic(msg)' would be shorter and also good.


I think that was one of the proposed variations, but we ended up picking the shorter .expect to cut down on repetition. We expected (ha) that this function would be used in these one-offs, so we wanted something more efficient.


As an old Java hack, my allowance for repetition is high. Especially when using and IDE that basically writes the code for me ;)


otherwise(msg)?


I would expect such a function to provide a default value


I actually initially had it as sort of a callback:

.otherwise(panic(msg))

(although I assume a rust panic! isn't really a function call).

But isn't the way to get the default, simply to use unwrap()?

In a simple script, failing to open a configuration file for reading is likely a show stopper, and you probably want to log/print an error (no such file, wrong permission, etc).

But in, say, a paint program, you'd normally not want to panic and crash if the image file a user selected to open in a file dialog is invalid or went away between the click-to-select and the click-to-open. In such a program you'd want to handle most file errors much more defensively.


It is, but also quite long, which I do not think suits Rust naming conventions.


Not really though: 'unwrap_or_else' is of similar length.


Not a Rust guy, but it looks like a way to say "this option value is required and if it is not present, crash with the following error". So "expect" is a fair name, since it means a value is expected and the absence of a value is unexpected.

I could see "required" or "require" as a better name. Or even just break the "positive names" rule and go with "notOptional".


Looking at it that way, I think it's possible to make "expect" more comfortable to read by how you phrase your error messages.

Instead of

    let mut file = File::open("conf.json")
        .expect("could not open file");
and

    let config: Value = serde::from_reader(file)
        .expect("config has invalid json");
and

    let jenkins_server = config.get("jenkins_server")
        .expect("jenkins_server key not in config")
        .as_str()
        .expect("jenkins_server key is not a string");
One could write

    let mut file = File::open("conf.json")
        .expect("Need to be able to open file `conf.json'.");
and

    let config: Value = serde::from_reader(file)
        .expect("The file `conf.json' must contain valid JSON.");
and

    let jenkins_server = config.get("jenkins_server")
        .expect("The config must have a key named `jenkins_server'.")
        .as_str()
        .expect("The config value of `jenkins_server' must be a string.");
Something like that.


That is a good point. I'm going to have to start being more positive in my expectations :)


I think it's like Assert. You pass an error message the same way you pass an error message to assert.


4 chained function calls from config -> read a key from it is quite an ask. Though I realize there's reason for it. There are many cases I'd be happy to have panics occur for invariants. config.strictString('foo') or something of that nature seems like it could be a more ergonomic choice in cases like thise.


What he is showing is how someone who knows Rust well, would potentially approach this problem.

What you're point out is that it's a large bar to ask a new comer to the language to do this because it requires a deeper understanding of the language to use.

Is it not appropriate to show that you can reduce the complexity of a program by using other features of the language?

It's not significantly different from reducing

   x + x + x + x
To

   4x


Each function call is a transformation on the previous argument. Whether or not you assign the results to a variable first before calling the second doesn't change the behavior of the code.

But yes, it does appear that the config library needs an extra wrapper that loads from files and returns configs (in a context) that does the common work for you.


I cant even opt out of using exceptions? Good thing I didn't waste my time with this lang.


You may have confused the method "expect" as being "except", as in "exception". That's not what it means - it means to "panic" with the given string as the error message. Panics in Rust have some similarities to exceptions in C++, but are not the same thing.


Exceptions don't exist in Rust.


expect is panic, not an exception


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.


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


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


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


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


It might be worth pointing out that this Rust:

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


I would love a `#[derive(From)]` for newtype structs and enum variants. Would bring Rust error handling back below Java in boilerplate levels. :)


There are some crates that add a derive for errors, but most people tend to use error-chain or quick-error. Both give you the boilerplate pretty much for free, with various other advantages. E.g., using error-chain, you can just write the above code as

    error_chain! {
        foreign_links {
            Io(io::Error);
            Parse(ParseIntError);
        }
    }
and it'll even generate an aliased `Result` type as well as fancy chaining support.


Right- error_chain is just a bit too much magic for me compared to what #[derive(From)] would do.



Very nice. Now we just need enum variants to be first-class types, and some syntax for deriving on them.


In 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;
    }


Sorry, 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?


You can definitely reuse other library's error types. I often do that while prototyping.

The benefit of lib-specific error types, though, is that they can be far more specific.


As an example of the kind of specificity you might want, take a look at the error type for parsing a regular expression: https://docs.rs/regex-syntax/0.4.0/regex_syntax/enum.ErrorKi...

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.


You can use pre defined error types. Have you seen https://doc.rust-lang.org/std/io/enum.ErrorKind.html ?


That seems to just be kinds of IO errors, though.


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

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.


We are actively working on rustfmt, which should add some consistency overall. Some people won't use it, of course, but many have said they will.


The kind of 'coding style' that is varying here is probably more in the domain of Clippy than rustfmt. But there are people working on that too!


Yes, that's what I meant. Many ways to do things and those ways can differ significantly.


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


There's no exception hierarchy. Knowing what exception something can raise is very important. Often, you don't.

Java has an exception hierarchy and exceptions as part of method signatures. If anything, Python's exception hierarchy started getting sorted out relatively recently.


Somewhat off-topic, but I'm curious to learn about the relatively recent changes in Python's exception hierarchy, could you point me to a link?


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


Exceptions don't need RTTI and have only runtime cost when thrown.


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


That is a compiler specific implementation, the ANSI C++ standard doesn't require it.


That'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 :).


Sadly I cannot pay for all commercial compilers out of my own pocket. :)


The ANSI C++ standard requires exceptions in the first place. The moment we start talking about disabling them, we're not talking about standard C++ anymore.


Which is why people cannot just state "In C++ the thing X happens" without regarding what the standard says, instead of what the installed compiler does.

Language != Implementation.

If the language would be "In GCC the thing X happens when ...", then ok.


For all practical purposes, the most popular implementations are the language. You can only ignore that in an academic context.


Popular where? Not everyone uses GCC.


> Exceptions don't need RTTI and have only runtime cost when thrown.

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.


> 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!().


It 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/


For reference, here is the link to the reddit thread:

https://www.reddit.com/r/rust/comments/69i105/


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


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


I don't get it. .unwrap() turns an undesirable situation into printing an error message and exiting with a nonzero status. There are plenty of situations where that is exactly what you want, and not a bug at all.

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?


Exceptions are well suited for parsing errors because they embed an error message.


Prints to where? stderr? Fine for a simple CLI tool, but people might want to do something more complicated when an "unrecoverable" exception occurs.

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.


...then don't use unwrap(). Instead check the error, do whatever handling you want and exit gracefully.

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.


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


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


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


> panics aren't for recoverable errors, in any circumstances.

And I understand that. Hence why unwrap everywhere is harmful.


Er, it was a response to this line of yours:

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


See above where one user is using .unwrap for parsing errors (input errors are recoverable).


I think this discussion is muddying the meanings of "recoverable" between "recoverable as a class" and "recoverable in this instance."

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.


Yes the problem is with the grey area between recoverable and unrecoverable.

.unwrap is only suitable for the unrecoverable case, while exceptions are suitable for both cases. Hence why .unwrap in examples is harmful imho.


Not in all cases. Most daemons will die if you give them an invalid configuration file (with a description of the syntax error).


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


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


Java's checked exceptions were a disaster. It essentially handcuffed you, limiting what you could do in an overridden method (because you can't add more exceptions to the throws list). So you end up wrapping in RuntimeExceptions and then later having the whole app fall over because the framework that's expecting your class was only designed to handle the checked exceptions.

So yeah, want your implementation to consult a database? Sorry, the interface you must implement doesn't declare any checked exceptions, so say hello to app-killing RuntimeException wrapped SQLExceptions :/


Checked exceptions are the correct answer. Just handle it.

Exception obfuscation frameworks (Spring) just kick the can down the road, creating problems without any discernible benefit.


> interface you must implement doesn't declare any checked exceptions

It verges on malpractice to design an interface and declare that no implementation of it could possibly ever fail. I'm looking at you, Runnable.


Checked exceptions probably would have been fine if there were only one kind of checked exception. Most methods would declare it and they'd all be compatible.

This would be similar to how Go functions always return the same error type, but without the boilerplate.


But wouldn't that kind of defeat the point? Sort of like how having a type system with only one type wouldn't be very helpful.


I ran into this when writing an ANTLR parser. I wanted to throw specialized exceptions based on what went wrong in the parse, but these couldn't be checked exceptions for the above reason. So I had to derive them from RuntimeException, and then I wrapped the call to the parser so that it caught just this kind of exception, and re-threw it as a non-RuntimeException so that it was checked throughout the rest of the code. I think this is an ANTLR-idiom, which I found to be an odd quirk - but one that's unavoidable because of how ANTLR and Java are designed.


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


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


a compiler-guaranteed safe systems language

When I need that, I use OCaml.


And when you need good support for multicore, you use...?


for and & in a shell script :)


Haskell and F#?


But OCaml has a non C-like syntax. So it would be even more strange to OP.


Being able to port a 20 line Python script to a 20 line Rust is the holy grail. Surely Rust has the ambition to one day achieve that, but it is by no means the main priority nor the original design goal of the language.

Justin criticizes the file_double function, it being complex with nested maps and conditionals. All of this complexity is also in the Python code, just hidden away in abstractions, the library and the virtual machine. Rust, right now, is still very explicit and revealing of inherent complexities. This code is exactly why you should use Python and not Rust for this kind of little script. One day the Rust developers hope Rust will be comfortable enough for you to consider using Rust in this situation, but it won't be soon.

The point gets softened a little by the remark that it probably would not be a picnic in C either, but I don't think even that is true. C still allows you to be very expressive, it would not encourage using those maps or even half of those conditionals. Rust is just that more explicit about complexity.

That said I honestly believe Rust is the best thing that has happened to programming languages in general in 20 years. Rust is rocking the socks off all the non-web, non-sysadmin fields, soon its community will make good implementations of almost every hard problem in software and Rust will be absolutely everywhere.


The file_double code was, IMO, quoted completely out of context. If you go read the error handling section, that code is used as a motivation to the next section, where you get gradually introduced to more economical error handling.

The error handling chapter is... Really big. Because it tries to explain everything from first principles. But it does include explanation and examples for using easy error handling as well, whose syntactic noise and level of boiler plate come quite close to Python.


Too much kool-aid. Most programmers are not writing system code and they'd be much better served with languages like Go, Nim, and D. In fact, the example the author is trying to port over would have been much easier in Nim.

The actual question is then about the author learning a new paradigm and way of expressing system code. If that is the case these are just pains he has to go through because Rust will never be like Python for quick and dirty scripts, nor should it be.


can nim handle gc across threads yet?


Yes. Use the Boehm-Weiser GC. Very simple.


".. soon its community will make good implementations of almost every hard problem in software and Rust will be absolutely everywhere"

This is very unlikely. I can't see when Rust would solve the problems Julia (for example) does. And vice versa of course.

Nothing wrong in a language tackling a few domains really really well and not trying to solve "every hard problem in software"


Rust hasn't made significant incursions into math modelling or industrial processing. It's advantages are slim there. Embedded will fracture into network interfacing and realtime where user input is less hostile, more predictable and doesn't require extensive constraint.


Isn't it this kind of thinking that leads to sql injections in number plate readers?


Many R/Python libraries for statistical programming use C/C++/Fortran under the hood, for the stuff that needs to be fast.

I'd like to contribute to many of these packages, but have little interest in learning the languages. Rust could provide a good replacement (and one which is more productive for people used to higher-level languages). There hasn't been much uptake so far, but I'm hopeful.


I'm hoping for matrix/tensor primitives, like Fortran. Rust might be a good replacement. The C interop would also be useful for Fortran interop.

What do you mean about industrial processing? PLCs?


  > import json
  > with open("config.json") as f:
  >  contents = f.read()
  > config = json.loads(contents)
translates to:

  extern crate serde_json as json;

  fn read_json() -> Result<json::Value, Box<std::error::Error>> {
      let file = std::fs::File::open("config.json")?;
      let config = json::from_reader(&file)?;
      Ok(config)
  }
And

  > import configparser
  > config = ConfigParser()
  > config.read("config.conf")
can be translated to:

  extern crate config;
  use config::{Config, File, FileFormat};

  fn read_config() -> Result<Config, Box<std::error::Error>> {
      let mut c = Config::new();
      c.merge(File::new("config", FileFormat::Json))?;
      Ok(c)
  }
Difficult stuff indeed.


I think this highlights that while there are easy solutions to the problem the OP faced, they're difficult for a newcomer to discover. (I have been using Rust for a couple of months and I'd also have reached for serde and maybe serde_derive to solve the problem).

Hopefully this is something the Libz Blitz[0] will solve with their Rust Cookbook[1]. (You could almost but not quite arrive at as simple a solution from chapters 1 and 2).

[0] https://blog.rust-lang.org/2017/05/05/libz-blitz.html

[1] https://brson.github.io/rust-cookbook/intro.html


The curious thing is that OP linked to the error handling docs, and the solution with Box<Error> is right there.


These struggles are real. I don't see a way around them other than just learning them (and then they go away, because you know what code won't work, and don't fight it).

It's probably because Rust looks and operates mostly like a high-level language, but still satisfies low-level constraints.

e.g. the confusing difference between `&str` and `String` is equivalent of C's `const char * str = ""` vs `char * String = malloc()`.

In C if you had a code that does:

     char *str = foo();
     free(str);
you'd know that in `foo()` you can't return `"error"`, since an attempt to free it would crash the program. And the other way, if the caller did not free it, you'd know you can't have a dynamic string, because it would be leaked. In Rust you don't see the `free()`, so the distinction between non-freed `&str` and freed `String` may seem arbitrary.


These struggles are indeed real, but at least as far as verbose error handling goes, remember that Rust is forcing you to handle a lot of things that are silently ignored in Python. Truly equivalent Python code would include a bunch of exception handling and checks for nil.


The author seems to want to not use unwrap() in the rust version when that is basically what is done in the python version. The python version will die with an exception if:

1) the file does not exist

2) the file is not readable

3) the file cannot be parsed

Rust forces you to say you want to panic in these cases (by using unwrap), but beyond that, the behavior is similar.


I haven't done much Rust, but why isn't there a safe way to do some of this implicitly? If you don't want to think about errors and just want to crash, then I feel like there should be a sane way to crash the script with a default error message. Could you write a thin abstraction to achieve this? Maybe there could be a new crate called "rust-script" or something, where the goal is to write code as easily as Python or Ruby.

Again, I'm not a Rust developer, but it's not hard to imagine an abstraction (or even a transpiler) that makes it easy to read a file, parse it as JSON, and do something with the data.


> If you don't want to think about errors and just want to crash, then I feel like there should be a sane way to crash the script with a default error message.

We have `expect` it's like unwrap(), but takes a string. When the program encounters some error, it will exit and print the string.


As the other comment says, that's exactly what's done with unwrap and try. You do obviously have to write that out, but on the other hand that means you know where to add the error handling when you come back after the fact. It might take a little getting used to, but it's a decent compromise. I'd say it's a matter of taste, but yeah, there are a few more characters there.


Couldn't there be an opt-in Deref for Option and Result? Then you could just pretend everything unwrapped fine.


There's unwrap() and panic!, does that not do what you want?


> It's probably because Rust looks and operates mostly like a high-level language, but still satisfies low-level constraints.

In an ideal language, you could decide to ignore low-level constraints and your code would work just fine, although perhaps less efficiently.


Exceptions are the best way to handle errors. You can either handle them everywhere or ignore them and they'll rewind the stack. Unfortunately Rust and Go decided to use return values, instead of fixing problems with exceptions, which is step back, IMO.


I agree with you but until there is an empirical basis for our opinion-probably-honed-by-years-of-coding, these 2 languages will just continue to chug along without real exceptions

My argument would be this: What is a runtime exception, really? It's a state that the programmer did not handle (either due to lack of thoroughness or flaws in mental model). Suppose the error is just ignored: To this I ask, why would you ever want code to continue in some state that has gone off the rails of determinism relative to the mind of the programmer? Imagine a bug that goes undetected because the corrupt state it generates only becomes a real problem many stack levels later under some corner case. Can you imagine a more hellish debugging scenario?


I can't speak for Go, but what you describe isn't really a thing in Rust. Functions in Rust that might encounter an "exception" typically return something like `Result<T, E>` where `T` is the type of what we hope we get and `E` is a type that encodes the details of the errors/exceptions we might see. It's an enum type that comes in two flavors: `Ok(T)` and `Err(E)`.

You can't just go happily along treating an `E` like it's a `T`, because the compiler won't allow it. So I'm not sure how you might get into a "corrupt state". You only have three choices: panic, return and pass the error back up the call stack, or handle it explicitly. All those choices short circuit what you were doing and don't leave you with anything that you could mistake for a valid result at any level of the call stack (again, this is enforced by the compiler).

Whether it's ergonomic is another question. I happen to like it, but for sure doing exceptions in Python means fewer LOCs, if that's what you're after. I tend to be more interested in how the features of a language help programmers to keep writing correct and maintainable code as the complexity of a project grows.


Good point. I am not very familiar with Rust (good to hear it seems to treat this well, however!) but a cursory examination of Go code shows that runtime errors can end up being entirely ignored and that seems crazy to me.

> I tend to be more interested in how the features of a language help programmers to keep writing correct and maintainable code as the complexity of a project grows.

So let me guess, you too have worked on very large spaghetti codebases? ;) Because I am also interested in that very same end-goal! And that is actually why I've decided to only focus on functional langs for now (right now it's Elixir but I'm going to be evaluating Haskell, not a huge fan of the JVM langs tho), because the resulting code just feels more maintainable


> but a cursory examination of Go code shows that runtime errors can end up being entirely ignored and that seems crazy to me.

I'm not sure whether this is a thing in Go, but I tend to think that having a ubiquitous null object in your language that's "falsey" is responsible for a lot of problems like what you describe and is basically a misfeature outside of C.

> So let me guess, you too have worked on very large spaghetti codebases? ;) Because I am also interested in that very same end-goal! And that is actually why I've decided to only focus on functional langs for now (right now it's Elixir but I'm going to be evaluating Haskell, not a huge fan of the JVM langs tho), because the resulting code just feels more maintainable

Sure have :). My personal opinion is that rich type systems like Haskell's (and Rust's, they are actually very similar in many ways) are really nice for managing complexity, maintaining and refactoring with confidence, and cutting off lazy design decisions at the roots. It takes a fair amount of experience (and I don't think I'm 100% of the way there yet by any means) to use them in a way that gets them out from under your feet and makes them really work for you, but it's experience worth having IMO.

Haskell is really great, IMO well worth learning even if you end up not using it professionally. Also, it's not built on the JVM; you might be thinking of Scala?


ah, I wasn't trying to imply that Haskell was a JVM language (I know it has its own compiler- GHC), I was referring to Scala and Clojure. I should have probably used a semicolon in between those two instead of a comma!

> having a ubiquitous null object in your language that's "falsey" is responsible for a lot of problems like what you describe

In practice I haven't really seen this causing problems, for languages like Ruby and Elixir where the only "falsey" values are nil and false. And it enables some very readable code.


>right now it's Elixir but I'm going to be evaluating Haskell, not a huge fan of the JVM langs tho

FYI, Haskell is not a JVM language. It compiles to binaries, the compiler is called GHC. Elixir runs on the Erlang VM, also unrelated to JVM.


I was not trying to suggest that Haskell was a JVM language. I said I wasn't a fan of the JVM languages (like Scala and Clojure), but I can see how someone might think that due to how I worded it, perhaps a semicolon instead of a comma would have helped, there


The subject of your sentence never changed, and the "though" (used as "however" in this case) make the topics related. I think it's more of a solecism than a difference in interpretation.

Glad we're all on the same page though haha.


I have run into something very similar in somebody else's python program where they declared a string that was later used (based on some conditionals) to locate a file. The thing is that most of the conditionals were never hit under ideal conditions (like passing all args etc.). It took me a lot of time to track down the bug (also because of the lack of awesome debuggers for Python).


> The thing is that most of the conditionals were never hit under ideal conditions (like passing all args etc.).

Your debugging work in this case might have also been alleviated by a good unit test covering this functionality.


It would have been. But it wasn't my code, I was merely using it. I think better structuring would have helped the code much more than unit tests. Unit tests are good to ensure you don't break things. But spaghetti code that passes tests is still considered broken by my standards.


> I think better structuring would have helped the code much more than unit tests.

This is why you TDD. Your code will automatically become more modular/structured by the very nature of having to unit-test it "right then and there" instead of hours later when you finished the component without any tests and decide to bang out a few basic integration tests and then head to happy hour and call it a day.

> Unit tests are good to ensure you don't break things. But spaghetti code that passes tests is still considered broken by my standards.

I agree, that's still technical debt. You literally cannot write spaghetti code if you TDD, though. You would feel a massive friction.


> Exceptions are the best way to handle errors.

Nope. At best, it depends on what kind of software you're writing and Rust is made for the kind of software where exceptions are the worst way to handle errors.


I have no opinion about whether exceptions are better or not, but .unwrap() is basically the same as an uncaught exception, no? Program closes, prints a backtrace and a somewhat cryptic message which is usually enough for a programmer to understand what happened.



Rust is targeting an audience where silently degrading performance is problematic.


I don't know Rust, or the general direction of the community around it.

Is there some chance, that over time, most popular functionality will end up in well architected crates that abstract away some of these complaints?

A bad example, perhaps, because they probably go too far with it, but a lot of java's verbosity fades away because there's a rich ecosystem of libraries that already know how to do what you're trying to do. There is, of course, a downside to that...important implementation details become opaque to the users of these libraries.


There is currently an effort to make Rust more 'batteries included' by auditing and improving many of the most commonly used libraries (emphasizing crates outside of std).


The was an easy solution that he failed to use. His return type could have just been Result<MyConfiguration, Box<Error>> and he could've used try!/? freely


My main gripe with Rust so far has been the unnecessary profusion of Result<> types, making it hard to process and forward errors.

Case in point: the example in the article from the rust documentation that converts errors to strings just to forward them: https://doc.rust-lang.org/book/error-handling.html#the-limit...

In practice, I find a type like Google's util::StatusOr (https://github.com/google/lmctfy/blob/master/util/task/statu...) a lot easier to use (I've written >100kloc c++ using it). This uses a standardized set of error codes and a freeform string to indicate errors. I've yet to encounter a case where these ~15 codes were insufficient: https://github.com/google/lmctfy/blob/master/util/task/codes...


> Case in point: the example in the article from the rust documentation that converts errors to strings just to forward them

This section:

- Shows you how to define your own Result types. They have chosen a String as an example of what you could use as an error type. In practice nobody uses "String" as an error type.

- Concludes by defining a custom error type to use instead of a String. I guess you didn't read that far? In practice nobody "converts errors to strings just to forward them". String was just an example they were using as they built up to defining a custom error type.

Rust errors can be forwarded as simply as "?". The conversions can be handled automatically with "From" traits. The "error-chain" crate takes care of these conversions for you, wrapping the original errors so they're still available (including stack traces), but aggregating them under a set of error types specific to your crate:

https://github.com/brson/error-chain


Using String as an example error type seems like a bad choice if nobody actually uses it in practice, though -- it's just leading you down the garden path.

Personally I found the error handling section of the documentation confusing and frustrating -- it works through three or four different approaches pointing out issues with them as it goes, and it's hard to tell when it's discussing a simple-but-wrong approach as motivation for the following more-complex-but-correct one, and when it's actually recommending you use the approach. Plus it finishes with an approach with nice properties but an awful lot of boiler plate conversion code, which left me thinking 'surely there must be a better way'. IMHO the error handling section of the rust docs should describe just one way to do things, and it should be the standard way everything uses so your code interoperates with library errors nicely, and that way should not require writing a page of boilerplate just to say 'my function might return an error from library foo or one from library bar or this error of its own'. (If error-chain is that one right way then it should be in the standard library and the documentation.) As it is it looks like 'this language isn't finished yet, come back in six months to see if it's any better' :-(


The reason why I wrote it that way was to motivate why error handling is the way it is. I personally think it's hard to just throw the "right answer" at someone and hope they get it, because error handling isn't some rote process you can just plow through. It's important to understand the case analysis involved so that you can choose the right granularity of error handling for your task. All of the error handling strategies in that chapter are interoperable to some degree (with perhaps "panic on error" being the odd duck out). They aren't incompatible philosophies.

With that said, thank you for the feedback. When I circle back around to it, I'll make sure to put more emphasis on The Right Way. The conclusion already has some of it, and the case study is supposed to show the progression in action, but perhaps more is needed.

I will let others focus on more targeted advice, since one huge chapter on error handling is only part of the story. The purpose of the error handling chapter is start with someone who might not even know what `Option<T>` is, and take them all the way through `try!`, the `Error` trait and automatic `From` conversions from first principles. More than that, it's supposed to teach you why using `String` or `Box<Error>` for your error type can be bad, even if it is ludicrously convenient.

Rust is a young language. I expect error handling idioms to evolve. Evolution doesn't mean something isn't ready to be used, because all languages evolve in some way.


I think having Result alone does not make error handling complicated, but having different error types for each operation (and concrete result) instead of using one generic error type for all of them does by pushing the job of unifying erros towards the user.

Go works around the problem by Error being an interface, which means any function can return any kind of error without needing to transform it to another form. However Go benefits from the Garbage Collector here - I totally understand why Rust libraries don't want to return heap allocated errors.

Maybe C++ std::error_code/error_condition provides some kind of middle ground: It should not require a dynamic allocation. And yet the framework can be expanded: Different libraries can create their own error codes (categories), and error_codes from different libraries can all be handled in the same way: No need for a function that handles multiple error sources to convert the error_codes into another type.

The downside is that the size of the error structure is really fixed and there's no space to add custom error fields to it for error conditions that might require it. A custom error type in Result<ResultType,ErrorType> can be as big or small as one needs.


> However Go benefits from the Garbage Collector here - I totally understand why Rust libraries don't want to return heap allocated errors.

This doesn't have to do with the GC or lack thereof. Instead it's part of the philosophy of zero-cost abstractions: idiomatic C libraries don't require heap allocations to return errors, so neither does Rust.


Note that Rust's Error is also a trait (similar to an interface) and Box<Error> does return heap allocated errors, and you're free to use that if you like. Being a trait object, it will erase the type of the error, but you don't have to write any of the accompanying From or Error implementations you normally would with your own ErrorType.


> instead of using one generic error type for all of them

But you can do that in Rust, just use Box<Error>. No library does that though, because it is not a zero-cost abstraction.


With the addition of `?` operator I think it's no longer the problem. It keeps error handling explicit, but the syntax is small enough that it doesn't make code noisy or tedious to write.

Note that you can also make your functions return `Box<Error>` which works like a base class for all common errors, so you don't have to worry about converting error types.


Sure, but if you want to actually check for some error condition (say distinguish between file opened ok, file not found, or some other error) - which is the whole point of having an error type to begin with, otherwise you could just use optional - you still have to look up the definition of the actual error type used every time.

A standardized error type used by everything removes that need - I know I can just call util::IsNotFoundError(..), no matter which library I'm using.


You may be interested in something like https://github.com/tailhook/quick-error. It lets you easily implement From traits for errors so that you can convert between error types.


> I've yet to encounter a case where these ~15 codes were insufficient

Insufficient on Windows.

There’re thousands error codes you can get from any Windows-provided API.

You can pack each of them into a single int32 value (using HRESULT_FROM_WIN32 macro for old-style error codes, the newer APIs already return HRESULT), but still, significantly more than 15.


Google's util::Status and util::StatusOr are actually quite flexible. While the generic error space is recommended for most work (and really, rather a lot fits in that space), it does support the notion of other error spaces like POSIX or Windows. At Google† I do quite a bit of interacting with the kernel, so my code makes fairly heavy use of the POSIX space.

That said, in the vast majority of cases any error I might be reporting from the POSIX space can be just as if not more usefully expressed (for the consuming software) using one of those ~15 generic codes. If their semantics are properly adhered to, those codes give good guidance on when an operation is guaranteed to have failed (but can be retried), when it's guaranteed to have failed (but cannot be retried without changing the request), when its fate is unknown, and when it has succeeded. In many cases this allows for generic error handling policies that fit a given application well. With enormous error spaces that is much more challenging.

In the cases where the underlying error deliveries clear value and I'm communicating across an abstraction boundary (I find the intersection of these is relatively rare), the Status type supports (albeit somewhat awkwardly) nesting. That allows the basic error to be one of the canonical types and the precise error to be communicated as a nested Status.

† I work on Google Compute Engine's on-host network devices and dataplane.


I mostly work on desktop and mobile software. “Unable to open the file: access denied” is helpful for end-user, will cause them to go fix filesystem permissions on the file they are trying to open, or restart the app elevated. “Unable to open the file: failed precondition” translates to “this software is broken, we don’t know why”

> With enormous error spaces that is much more challenging.

Not sure I understand the problem. On Windows, APIs are typically designed as reliable (this applies to both OS API, and the way third-party developers design stuff). If something is failed but the condition is temporary and might have fixed with retry, well-designed API will retry itself, possibly accepting timeout argument. That’s why you can do generic error handling just fine: FAILED() macro is enough for 99% cases.


A common theme I find in posts criticizing Rust is that their authors take a problem they've already solved in another language, try to blindly convert it in non-idiomatic Rust and then complain because things get awkward.

I think that it's important to pick the right tool for the job and to follow the patterns of the tool you're using.

Is Rust the right tool for the task described in the post? Probably not, but it could still be used albeit it will always require more work than Python.

What's really missing is a resource showing common problems and their idiomatic solutions.


The `error-chain` crate [1] exists to get rid of precisely the error handling boilerplate the author has encountered. That's not ideal, though, as I believe that a place for such functionality is in the core language, not a separate library, but it gets the job done. As for the `let mut file` bit, that makes sense to me: a file in the standard library is an abstraction over a file descriptor in the operating system, and the descriptor has a file pointer which must be advanced when you read from it. I don't consider it internal state; the read operation will return new data every time, so it's not a pure function. It follows that in order to behave that way, it has to depend on some pretty explicit state.

As the other comment said, Rust needs to make some trade-offs, because you simply can't have an expressive and easy-to-use language that runs so close to the metal and is aimed at being C++-level fast. As such, Rust will never be as easy to write as Python, and for scripts like the author mentioned, I'd say that Python is a much better choice than Rust.

Rust is, by design, a systems programming language and it does have complexities and gotchas that arise from the need to have a lot of control of what actually happens at the machine code level. If we had a Sufficiently Smart Compiler(tm), of course, you wouldn't have to worry yourself about those low-level details and just write what your program needs to do and nothing more. However, in the absence of such an ideal, we must accept that a high-level abstraction must always leak in some way in order to let us control its operation more closely to get the performance we need. In my opinion, it's much better that necessary abstraction leakage is made a deliberate part of the API/language and carefully designed to minimize programmer error, and Rust, I think, does a good job of doing exactly that.

That's not to say that the language cannot be made more ergonomic. For one, I think that rules for lifetime elision are a bit too conservative and that the compiler can be made smart enough to deduce more than it currently does. I'm also excited about the ergonomics initiative, and I hope that the core team will deliver on their promises. In general, as someone who's written more lines in C/C++ in my life than any other language, I'm very excited about the language as a whole, as I think it provides the missing link between those languages that are expressive, high-level, and reasonably safe but slow, and those that are fast, low-level, a bit terse, and allow one to shoot oneself in the foot easily.

[1]: https://crates.io/crates/error-chain


I'd like to see error-chain standardized into the standard library as well. It seems by far the most sensible approach to building Error types.


Conversely, I really hope it doesn't. error-chain doesn't implement `PartialEq` which means that I can no longer write tests for my failure types. I'm a big believer in quick-error instead.


There are some good `assert_match` macros that allow you to pattern-match error cases instead.

Might also be possible for error-chain to implement PartialEq if you didn't want some of its generalized error-boxing bits. If that's something you need, you might file an issue.


I'd prefer it if that didn't happen, or at least not for a few years. error-chain involves some pretty hairy macros, which would feel out of place in the standard library. I'd like to wait and see if the community can come up with something more transparent before canonising error-chain.


https://nim-lang.org/docs/parsecfg.html

(Scroll to the examples.)

An exception would be thrown on error. That exception could be trapped in a simple try/catch block.

Nim is very similar to Python -- but statically typed and compiled (quickly) to machine code. There are many situations where Python is a better choice than Nim, but if you're looking to translate Python code for speed and type-safety, Nim is worth considering.

And if you want to translate Python to Nim gradually, look at this magic:

For calling Nim from Python: * https://github.com/jboy/nim-pymod

For calling Python from Nim: * https://github.com/nim-lang/python/tree/master/examples


The big question is would the Python script crash or handle the error when obvious problems like not valid JSON or file not found happen?

My experience with Swift vs Objective-C is that clean Swift is crash free but more verbose when all other things are equal.

If you don't need that level of security because it's just a small script Python was the right choice.


Depending on where it crashed, the Python script would raise an exception. It would most likely be an `IoError`, `KeyError`, or `ValueError`. Then it would show an error message with a line number, column number, and traceback. Using a debugger would allow you to step backwards through the traceback to determine if the error was caused by something further up the line or where the exception was raised.

All of Python's exceptions are an instance of `Exception`. In order to catch and handle any exception that can be raised, you can use a `try, except` block with the base `Exception` class. This, however, is bad practice as there may be some exceptions you want to ignore and, generally, you also want to print a different error message depending on which exception was raised.


So the Python script proposed in this article really just skips all error checking and will die just the same as the hard unwrapped Rust version with the only benefit it actually produces a more user-friendly error and it doesn't look as ugly.

I can't edit my reply anymore but the question was actually meant to be rhetorical rather than I really wanted an answer.


> So the Python script proposed in this article really just skips all error checking

Only if the programmer chooses to not handle exceptions, which is bad practice.

>and will die just the same as the hard unwrapped Rust version with the only benefit it actually produces a more user-friendly error and it doesn't look as ugly.

No, it won't die unless the programmer wants it to. In Python you can catch an exception and continue the operation in a different manner. In fact, it is common practice in a lot of Python libraries to use exceptions to determine the presence of data and act according to whether or not the exception occurred. If I'm not mistaken, I believe that this is common practice in most languages that utilize the exception pattern.

> I can't edit my reply anymore but the question was actually meant to be rhetorical rather than I really wanted an answer.

Well, you seem to not know a lot about exception handling in Python, so I hope I was at least a little helpful.


It would crash but would print an error message along with a stack trace. The rust version will probably just crash with a confusing error.


Why would the Rust version crash?

The compiler will warn you if you if you haven't used a result type, and it's up to the programmer to decide what to do in the case of an error, just like when handling an exception.

If you were going to use the JSON result for something, then you are forced to check if the result was Ok or Error. The only time you'd get a crash is if you just called .unwrap(), and even then you'll also get a stack trace.


Note that you only get a stacktrace if you set the environment variable `RUST_BACKTRACE=1`. Otherwise, you get a standard panic message. The message from `unwrap` is probably unhelpful, which is why a lot of folks advocate using `expect`.


I think the point of the question was that in Python errors can still happen but they tend to get ignored when writing small scripts. So if one were to explicitly ignore errors in Rust how would the resulting program's behavior differ from the Python version?


It would just raise an OSError/IOError/TypeError/ValueError and exit (unless caught).


Rust's aversion to exceptions is exactly like Go's aversion to generics - a strongly held position that doesn't actually make anyone's life easier.


I find result types to be much easier to understand and work with than exceptions. Result types can be handled by the type system, even when you have checked exceptions in java, there are still exceptions that aren't checked, and the syntax for the checking becomes monstrous.


The anti-pattern I've seen in dysfunctional enterprise development shops (i.e. most of them) is that checked exceptions mean exceptions that "we'll never have" get buried lower in the stack; so code can fail silently and continue running just to avoid the monstrous checking code and propagation of exception type declarations up the call stack.

I don't see how the Rust approach would avoid this fate but I doubt it will ever be used in these contexts to begin with.


I think that part of it is that the idea of a Result type being normal will help prevent much of the cruft and burying we see with exceptions. I also feel like handling Result types is more natural than exceptions.

First, you _have_ to do it, even if that means a try! and passing the buck. The syntax for this isn't as monstrous as it is for checked exceptions as well.

Second, it feels more like a natural code-flow, not the break that exceptions provide.

Third, it's useful for more than just "Exceptions". Coupled with Optional types, it provides a more expressive way of not-hapy-path-code where exceptions just feel heavy handed. For instance https://docs.python.org/3/library/stdtypes.html

     d[key]

        Return the item of d with key key. Raises a KeyError if key is not in the map.
The key not existing isn't really exceptional. A proper optional, union, or result type handles this case much more easily.

In sum, I think the expressibility of Result and Option, along with a more natural flow for handling them will make working around them less tempting/viable/easy to pass over in a code review.


You don't have to convince me of the value of Result types, I prefer them too. I've only written a couple thousands of lines of Rust but tens of thousands of Haskell which rely on the same concepts (though its nicer with do notation). I don't have faith in the masses though and right now there is a strong selection bias in Rust that means only people concerned with quality and correctness are using it in the first place.

I think twenty-five years ago there were similar hopes and dreams for Java checked exceptions.


The big problem with Java checked exceptions was that the language was simply not flexible enough in other areas to handle higher-order stuff. For example, quite often you want to define something like "this method takes object X, and throws everything that X.foo() can throw, and also E". But there's no way to express it in Java. So the moment you start doing any sort of HOF-like stuff - even as simple as event handlers - you have to struggle with checked exceptions.

This is not a problem in Rust.


> I think twenty-five years ago there were similar hopes and dreams for Java checked exceptions.

Yeaaaaaaah. I remember being a very strong supporter of checked exceptions before I got into a giant codebase and saw how not having speced a sane exception type can cause lots of pain and some really long declarations or dropped exceptions.

   catch (Exception e) { throw new RuntimeError(e); } 
:(

You're right, I guess time will tell.


Exceptions in Python are used rather differently than in most other languages. Their overhead is much lower than exceptions in e.g. Java or C++, so they're routinely used to indicate expected conditions, like missing key in dictionary, or end of iterator. Since the language is dynamically typed in the first place, there's no concern about it effectively circumventing the type system.

That said, dict also has get(), which lets you specify the default value if key wasn't found (and the default default is None).


> fail silently

No. They are usually logged. The difference is that with exceptions, the logging will occur at a higher level and be done uniformly while in Rust you'll have to explicitly thread the error through the call stack manually to log it at a higher level.


On that point, does that mean Rust libraries tend to have logging hooks or are all errors emitted by Result's?

What about non-fatal errors such as retries in a GUI library that loads an image from the web, how are they logged or otherwise propogated to the developer?


Errors in Rust are just normal values, so there's nothing special you have to do. You just write your retry logic, presumably after inspecting the kind of error that occurred.


I'm aware of how Result works, I've written a little Rust. I was wondering how Rust libraries specifically deal with non-fatal error handling in parts of the library that are abstracted away from the library consumer, such as my example of a GUI library that has retry behavior that is not exposed to the user.

Is there currently a convention for dealing with such "encapsulated but interesting" errors occuring in Rust libraries?

It seems obvious to me that a web framework would have a logging hook but non-obvious how that API would function; would it call a logging callback with a severity and a string? Just a string? Or an error message and some kind of "related data" (such as a stack trace or relevant structs) container?

It doesn't seem obvious to me to log only text in the context of a GUI library. I'm thinking of building a native cross-platform GUI library in Rust (borrowing concepts from IUP[0] but adding more typing) and I feel like there would be value in having structured data as part of the nonfatal error interface, so I'm curious if there are existing patterns to learn from.

[0] http://webserver2.tecgraf.puc-rio.br/iup/


Ah, I see.

For logging, the `log` crate[1] provides the interface that libraries can use, which defines macros for each log level.

For partial success/failure, I actually don't think there is much convention. On the one hand, you might consider logging as sufficient enough, depending on your use case. In some cases, I have adopted a form of partial errors. That is, instead of:

    fn foobar() -> Result<Value, Error> { ... }
I use

    fn foobar() -> (Value, Option<Error>) { ... }
You can see an example here: https://docs.rs/ignore/0.1.9/ignore/gitignore/struct.Gitigno... And in particular, the error type is a recursive structure, which permits it to store an aggregation of errors: https://docs.rs/ignore/0.1.9/ignore/enum.Error.html

Since this isn't something people need too often, the syntactic overhead of this approach is considerably clunkier, so I definitely wouldn't want this to be a pervasive part of a library. Nevertheless, if you can get both a success value and an error value, then your return type is inherently a product, not a sum, which is at odds with the `Result` sum.

[1] - https://docs.rs/log/0.3.7/log/


Implicit return codes (e.g. return int, -1 means error, 0+ means OK) are equivalent to unchecked exceptions. Explicit return codes, where you must process them or compiler will yell at you are equivalent to checked exceptions. I think, that checked exceptions are a good idea, but they must be improved. E.g. Rust have syntax for almost implicit converting one error to another and return it; checked exceptions could use similar approach, so you can declare another exception in your "throws" cause and compiler'll generate conversion code for any unhandled checked exception.

Anyway for me exceptions are way easier to work with, than return codes.


Yes, exceptions are better than return codes, but I would argue Result types are better than exceptions. Checked exceptions in Java have their issues. Exceptions in C++ are odd beasts (though that's getting better, they're still not checked and therefor basically anything that isn't noexcept can throw them (oh wait! noexcept can throw! it just crashes immediately)).


That equivalence is false. An unhandled exception bubbles up. An unhandled return code is ignored. This was exactly one of the biggest arguments against return codes.


We nearly had a hostile language fork over mandatory unwinding. It is not something that the language team decided and refuses to acknowledge. Rather, we had to respond to the demands of our embedded users.


Fair enough.


Monads are a superior form of error handling than exceptions... The only problem is that AFAIK (but I'm still learning it) Rust is missing something equivalent to Haskell's do notation.


> The only problem is that AFAIK (but I'm still learning it) Rust is missing something equivalent to Haskell's do notation.

Rust has essentially specialized do-notation for error handling called the `try!` macro, or more recently, `?`. (`try!` and `?` are exactly equivalent in today's Rust.) Actually, it does just a bit more than standard do-notation would: it also tries to convert your error value at the call site to the error type expected by the return type of the current function.

The problems posed in the OP are pedagogical ones IMO that I hope can be solved. I think the current resource on error handling in the book is good for folks who really want to dive in and figure out the complete story, but it's bad for folks who just want to write code that works without spending a couple hours doing a deep dive. So I think there's room for more targeted pedagogy here.


> Monads are a superior form of error handling than exceptions

No, they're not, not by a long shot. As soon as you start having multiple monads returned by functions and you need to combine them, you need to introduce monad transformers and your code turns into a giant untractactable spaghetti mess.

Exceptions have issues but they are the sanest way to handle errors today.


Well, if you start doing more with monads than error handling, you will have complex interactions between them.

They are still superior for error handling, exceptions would have a more complex interaction with them.


I honestly found error codes in go to be pretty pragmatic. You pay a small cost on the writing side, but it's very clear and straight forward.


The Go people don't have an aversion for generics, just a very conservative approach to adding language features. To quote their FAQ:

"Generics may well be added at some point. We don't feel an urgency for them, although we understand some programmers do."[1]

[1] http://golang-jp.org/doc/faq#generics


Sounds like an aversion to me. By the time Go was developed, there was no doubt in my mind that generics (or some kind of polymorphism along those lines) were an essential feature for any new statically typed programming language. The fact that Go's developers "don't feel an urgency for them" to me makes it sound like they are living in the 1980s.


> were an essential feature

Do you mean, "this feature is required for me to write code in that language"? Or do you mean, "this feature is required for any project in the language to flourish"?

If the former, why do you think your preferences generalize? If the latter, how do you explain the large number of successful Go projects? Are we all stuck in the 1980s? And if so, what does that even mean?


Well, at the time I would have thought it was required for the language to become widely used. But since that's clearly not true I suppose I have to downgrade that statement to say it's required for me to write code in the langauge and not feel like I'm constantly banging my head against a wall.

Frankly, yeah, I think Go programmers are kind of stuck in the 1980s in some respects. This isn't something I'm completely clear about, but I feel like Go's developers are biased against anything that smells at all academic. So for instance they didn't want to implement a super fancy, cutting-edge type system. Which is somewhat understandable... but as a result they ignored 30 years of programming langauge research and implemented a primitive type system that basically provides nothing over C. I do not understand this mentality.


The payoff of the simpler type system is that people don't write posts like OP's about Go. The language has its rough edges, but it's not a brick wall the way Rust is -- you can learn the idioms and become a productive Go programmer very, very quickly. On top of that (or maybe as a related consequence), most Go code looks roughly the same. If you dive into the source of one of your project's dependencies, you aren't likely to find some esoteric or too-clever-by-half coding style. There are no fancy macros to untangle, because the language doesn't support them.

Writing that sort of clever, super-concise code scratches an itch that a lot of people have (myself included) but it's not something I want to encounter when I'm trying to debug something. When you're working with other people's code, you want it to be simple and consistent. That's what Go's primary strength is.

btw, I'll take this opportunity to plug my own "Go generics" solution: https://github.com/lukechampine/ply. It's like a Coffeescript for Go that lets you use stream HOFs like map/filter/reduce without any runtime cost.


I've used both Rust and Go daily for years (before they reached 1.0, respectively) and I'm generally happen with the experience that they give me. Not all languages need to have a type system as sophisticated as Rust's or Haskell's.

> but as a result they ignored 30 years of programming langauge research and implemented a primitive type system that basically provides nothing over C

As someone who also has a decent amount of C experience, I don't see how this is a reasonable conclusion to make. The first point in favor of Go is that it's mostly memory safe (sans data races), which is achieved not just with bounds checks, but with a stronger type system. The second point in favor of Go is that Go does have a limited form of polymorphism that is checked by the compiler. In C, the only way you get polymorphism is by subverting the type system entirely. Both of those points are huge, and there's undoubtedly a much longer list that I could craft of smaller benefits if I were so inclined.

As we march toward complex type systems that move more runtime errors to compile time, we must also be vigilant of the trade offs we're making. Moving things from runtime to compile time isn't necessarily free; there's almost always some new cognitive load that is implied. It's important because if that cognitive load is "too high," then people aren't going to switch to it, and no amount of navel gazing is going to fix that. This entire thread is a perfect demonstration of that trade off in action. On the one hand, we have the "clearly superior" Rust approach to error handling that checks a lot more at compile time than Go does, but on the other hand, Go programmers don't ever need to "learn error handling" at all. They have a simple convention with a reasonably low bug rate (IME, anyway).

We can't just judge programming languages by their theoretical strengths. We must also judge them by their practical strengths. And then we must do the impossible: balance them. Snubbing our collective noses isn't going to do any good. Our only hope is to understand why people are attracted to languages like Go (such as myself) and figure out how to reduce the aforementioned cognitive load without giving up those sweet sweet compile time checks. Rust is clearly pushing that boundary, and I'm happier for it.


>On the one hand, we have the "clearly superior" Rust approach to error handling that checks a lot more at compile time than Go does, but on the other hand, Go programmers don't ever need to "learn error handling" at all. They have a simple convention with a reasonably low bug rate (IME, anyway).

I don't think you misjudges the differences in complexity here at all. But wouldn't that have more to do with the different approaches to memory management?

>aforementioned cognitive load without giving up those sweet sweet compile time checks

I would argue the cognitive load comes mostly from nominal typing and lack of type inference. A counter example would be the Crystal language which has a very strong type system, but is exposing barely any of it as 'added cognitive load'.

This correlation between 'cognitive load' and 'expressiveness of a typesystem' seems unfair. Having to keep track of all the patterns that are valid but not supported by the type system is also a type of added (hidden) complexity.

Even the poster-feature of Rust has a (IMO) much easier cousin with the same (and more) advantages called 'the Clean programming language' in the shape of 'uniqueness types'. And most of the complexity in Rust is the result of the type system not being expressive enough for how they are attempting to use it, rather than the opposite.


> But wouldn't that have more to do with the different approaches to memory management?

What makes you say that? Rust's approach guarantees more type invariants.

I don't buy for a second that type inference has much to do with this. Firstly, Rust has type inference, it just isn't global. Secondly, I've found that type inference everywhere makes the cognitive load much worse, not better.

And I wasn't actually drawing a correlation between cognitive load and expressiveness. Namely, expressiveness isn't the final word. Much of PL theory is devoted not just to improving expressiveness, but making that expressiveness more accessible to the masses. Compare Haskell with System F, for example.


>And if so, what does that even mean?

The lack of generics in Go simply means that you will for common algorithms and functions end up with either of these scenario's:

1. You were forced to duplicate code under a different name with a different type signature (even though the body would be identical)

2. You are forced to use the 'empty interface' which negates the advantages of static type-checking (and seriously: why isn't it the default when you open an accolade?)

Not having generics in a statically typed language means you combine the disadvantage of static typing ('having to specify types and doubling the cognitive complexity of the language with a special type syntax') with ('not having the compiler be able to check the validity and risk run-time type errors, ie. things like cast exceptions').

>If the former, why do you think your preferences generalize?

Generics is not a platonic ideal such as 'object oriented programming'. Type systems are as solid as math itself. And just like Math there are multiple systems with different trade-offs in how expressive they are. This expressivity is not subjective! One type system can be strictly more expressive than another type system. Consider this pseudo-code without any type annotations:

    def example(a,n)
        return a.something(n)
In a dynamically typed language such as Ruby, Erlang, Python or JS there are programs that would call this method that would make it throw an exception during run-time. For example because it doesn't know how to do '.something' on the provided argument.

The challenge: It barfs during run-time and we want it to barf during compile-time.

Now imagine all the possible usages of this method in a dynamically typed language that would be provable correct. All the potential combinations of a's and n's that would not make it barf. How many of them are legal Java, Haskell, C# or Go programs? (some, most, some, few)? The goal of a static system is to barf during compile-time. Not to exclude valid, legal programs just because the language author thinks type systems are hard to implement.

Whenever you can write provable correct code, that a type-system is complaining about: how is that not a bug?

>If the latter, how do you explain the large number of successful Go projects?

1. The projects are successful because of the people who make them

Well for starters by not assuming that people who program in Go are completely incompetent drooling idiots who are only able to deliver working code because Go is so great. As if the same developers wouldn't have successful projects right now, if Go didn't exist.

2. Any language released by a famous language author at Google will get a large minimum cult following by default

Much like with Angular who releases something has a strong impact on the adoption, regardless of the actual quality of the product, language design or implementation. I'm convinced the same set of programmers would be more productive in other languages. I'm also not surprised they themselves aren't aware of it.

3. Go has nice competitive features, the type system just isn't one of them.

Finally, Go isn't all shitty, for example, it has a very nice IO and concurrency model and ever since it finally got a precise garbage collector, one could finally use it long-running processes.

4. Your logic has clear type errors that a strong type system would help you catch

The existence of successful Go projects isn't an indication of _anything_. With the same logic you could argue VHS is superior to Betamax or that that Kanye is a talented musician. It's like the Silicon Valley variant of religious logic of 'moral people don't get sick'. Sometimes people, projects or products are not successful because they did everything the right way! Sometimes they just get lucky. Sometimes they just plain conspire against civilisation and cheat. Sometimes the things they did correctly ended up being more important the the things they did wrong. Sometimes they just get to announce on a bigger stage than somebody else.

> Are we all stuck in the 1980s? And if so, what does that even mean?

It means that there has been a large research initiative since the 1980's in software verification but because science is hard and thinking is hard and we are all getting paid anyway, we rather prefer to cargo-cult, bike-shed and gloat about our own ignorance. Because what you haven't learned yet is hard and what you already know is easy, even if it's all wrong.

It doesn't help that most of the academic researchers don't care enough about engineering to turn their research into actual production tools (with a few exceptions).

But i'm starting to warm up to the common notion that all progress in programming language design comes not from people learning, but from generations of developers eventually dying of old age. There is just too much money to go around for developers to not act like spoiled ignorant little children, and as a result most technology is just fundamentally broken and for no good reason.


I think you kind of missed my point, and crucially, missed that I was specifically questioning usage of the word "essential." You very clearly read far more from my comment than was said or implied. See: https://news.ycombinator.com/item?id=14286020

Personally, I think a lot of the language you chose to use in that giant wall of text was quite unfortunate. I'm not interested in being a party to your ax grinding.


But Go does have polymorphism. It is achieved through the "interface" concept, which allows dynamic binding of any statically typed objects that match a given set of function signatures. In my experience, with the way it's been done it actually gets you pretty far in terms of problems you typically solve with generics in other languages.

That said, personally I'd love to have generics on top of that. Consequently, I have been following some of the discussions on the topic, and so far I haven't seen anything suggesting that the language developers have an aversion to it. What they do have, however, is a fear that an improperly designed generics concept could badly screw up the language in a way that can't be reversed by any practical means once it is launched.

They are basically very careful about adding stuff, without fully understanding the consequences at all levels. You (or I) can disagree with that approach, but it's not accurate to say that they have an aversion for generics, or even to say that they don't want it.


It is exactly the same kind of polymorphism that languages like Java and C# had before they adopted generics. In practice, what this means is that you have downcasts all over the place, which is tedious, non-typesafe, and is why both C# and Java have generics now.

The fact is that Go is the only statically typed language, with any claim to being mainstream, that doesn't have generics. And it's not like generics are some kind of a new and radical concept. Java had them for 13 years now; C# had them for 12. There's literally millions of lines of code written in popular languages that utilize generics, which can be used as a guide to proper design, and understand its consequences.

The idea that there needs to be more "baking time" for generics simply doesn't hold water at this point. It amounts to insisting that structured programming (loops etc) should not be adopted "without fully understanding the consequences at all levels", and meanwhile we'll just use if+goto - in 1980.


If you want to understand the reasonings the Go maintainers have about generics, there is a GitHub issue[1] on it that is probably a better place to start than to argue with me. The thread references both recent academic research as well as other programming languages' take on the subject.

I don't think their crux is so much that generics "as such" needs more baking, but the specifics of how to implement them with the Go language. Mind you that some of the core goals of Go is to be simple, easy to parse, fast to compile, support good tooling, etc. so the question they're battling with is how to add generics to that mix without sacrificing any of those goals, and without making some mistake you can never go back from once every code base out there starts depending on it.

Now, by all means, we can argue that those priorities are wrong, or that yours would have been different. But I think it is disingenuous to suggest that they are effectively idiots who don't understand how to apply basic concepts, or are unaware of other programming languages.

[1] https://github.com/golang/go/issues/15292


No-one is suggesting that they're idiots, though. The original argument is that Go designers are very conservative, and extremely averse to some language features that aren't even "new" anymore - not that they're unaware of those features or how they work.


> It is exactly the same kind of polymorphism that languages like Java and C# had before they adopted generics.

That's false. Go provides a smattering of blessed polymorphic types (slices, maps, chans, pointers) and functions (len, append, delete, chan send, chan recv) that go a long way. They are horrifying to civilized PL enthusiasts, but they cover a lot ground.

As I said in one of my sibling comments, navel gazing isn't going to get you anywhere. And Go isn't the only statically typed language without generics. C has that designation as well.


C is one of those things that hasn't meaningfully changed in decades now. So yes, technically it is a mainstream language without generics (and many other things) - but I don't think that's a viable role model. The only reason why C is what it is, is because of all the legacy code written in it.


Who said anything about C being a role model? I think your chosen language is somewhat misleading, so it's worth pointing out.


Exception safety is complicated. I didn't believe this, having only used exceptions in higher-level languages, but enough time talking to Rust folks and I get it now. And even in C#, I know I'm getting things wrong here and there with exceptions, but it doesn't have the same impact (safety/mem leaks) due to being GC'd and memory safe.


The author doesn't really justify why he needed to port the python script to rust in the first place.

Pulling down some JSON, doing a bit of transformation and sending alerts seems like a perfect candidate for a high level language, I don't see any reason why you would port it to Rust unless you had significant performance concerns


> The author doesn't really justify why he needed to port the python script to rust in the first place.

And they don't need to. When I first learned Rust, I tried to write a `filter` function. Why would I ever do that? I could write `filter` much easier in Python, or heck, just use the `filter` method on iterators that is already in the standard library. I did it because I saw it as an opportunity to learn. I wanted to connect something I knew (`filter`) with something I didn't know (Rust).


I guess I was getting at the fact that porting the python script that does something very simple is the wrong tool for the job.

A good candidate for trying to learn Rust is to find a script or tool that currently has the problems that Rust claims to fix.


When someone comes to me and says, "I just spent the weekend porting a simple Python web crawler that was working just fine to Rust, and I came across problems x, y and z." What do you think an appropriate response is? Should we berate them for "choosing the wrong tool for the job"? Or should we ask them what they're problems were and help them fix them?

I'm not saying we shouldn't have a conversation about which-tools-are-appropriate-when, but when someone is obviously trying to learn, let's put that on the back burner.


Before I convinced co-workers that we could use Rust in production (where we'd normally have used Python), I wrote a lot of this kind of script (where, I'd agree, a higher-level language like Python is a better choice) in Rust and Python so that they'd be able to have some idea of what the differences were and how Rust works. It seems like a great exercise for the very conservative or intimidated programmer to ease into more low-level programming.


>I don't see any reason why you would port it to Rust unless you had significant performance concerns

To learn Rust in a well-controlled environment?

Isn't that the usual/sane way to learn a new language? Take something trivial you understand well from language A and port it to new language B?

Note, of course, that this doesn't imply the result should be put into production.


Applications are open for YC Winter 2020

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: