My Struggles with Rust(compileandrun.com) |
My Struggles with Rust(compileandrun.com) |
Though, I wish it has less exotic syntax. It's like C++ and Erlang had a baby. Look at modern languages with nice syntax like Go, Julia, Swift and compare it to Rust. Someone coming from C, C++, C#, Java, PHP and JavaScript has to learn a lot of new syntax twists that look uncommon and different for little reason. Sure some overly complex early syntax ideas like different ones for different pointer types vanished in newer Rust releases. Now it's probably too late to improve the syntax.
It's much closer to C/Java/JavaScript than e.g. Python or Bash are. Rust still has curly braces for blocks, uses ampersand and asterisk in ways that aren't too far from C, uses dot in a way that's not too far from C or Java and uses less-than and greater-than to denote generics like C++ and Java.
Personally, what keeps tripping me up when moving between languages is either forgetting to put parentheses around "if" conditions in non-Rust languages after writing Rust or having the Rust compiler complain to me about unnecessary parentheses after writing non-Rust code.
But if new languages couldn't do things like omit unnecessary parentheses around the "if" condition or improve readability by moving the return type to come after the function name, that would seem like too big of a restriction on trying to make some syntactic progress.
Edit: Plus it makes sense to have types after the variable name when they are optional in most cases (and then to have them in the same order in function signatures for consistency).
Since you brought this up, the one thing that really (but irrationally!) winds me up about Rust's syntax is that variables are:
let foo: Bar = ... ;
And functions are: fn foo() -> Bar { ... }
Why not use a colon for the return type of a function as well?Not that I am suggesting that you are the "hater" here.
Who are these mean Rustaceans?
Is Nim considered such a threat to Rust?
PS. To my earlier downvoters can I have my hard won karma back, please??? This is the response I have been advised to proffer after consulting on the Nim forum, after my earlier terse comment.
- In your first example, foo is of type "Bar"
- In your second, foo is of type "function that returns Bar".
ie, if you stored your foos in separate variables they would be:
let my_var: Bar = ...
vs let my_function: fn() -> Bar = ...
If they were to change it to your suggestion the latter would be either awkward (multiple colons?) or inconsistent (different syntax for types and declarations).2. If one needs variable to hold a function, he declares it like this:
let foo: fn(i32) -> i32 = ...
The second colon would be confusing.Can you see how that it's not helpful, or a good addition to the conversation?
If you'd have provided some good advantages of Nim in this case, or in general added to the discussion at hand, you might have gotten less downvotes.
Maybe you want to stop attacking a community directly, aswell... last time I checked this site wasn't Rustacean only.
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".
* 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".
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
This was my feeling when I got into Ruby on Rails (years too late).
Wonder why the difference?
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.
(Which probably means your first project should be something open-source so you can share your code easily)
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.
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.
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.I guess I'm just surprised people think that Rust should be as simple to use as Python. Maybe I'm wrong.
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.
> 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.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).
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.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...
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.
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.
(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
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.
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.
I can't edit my reply anymore but the question was actually meant to be rhetorical rather than I really wanted an answer.
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.
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
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).
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.
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.
...because people use it; and then say; 'but don't use unwrap...'; and then use it, and your 'safe' language then happily crashes and burns everytime something goes wrong.
Blogs and documentation are particularly prone to it.
Result and option types are good; but if you're gonna have unwrap, you basically have to have exceptions as well (or some kind of panic recovery), because, people prefer to use it than use the verbose match statement. :/
require "yaml"
config = YAML.parse(File.open("test.yaml"))
[1] http://crystal-lang.org fn gimme_config(some_filename: &str) -> MyConfiguration
{
toml::from_file(some_filename).unwrap()
} 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...
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");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...
I could see "required" or "require" as a better name. Or even just break the "positive names" rule and go with "notOptional".
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 4xBut 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.
At this point the cognitive load required to read and understand Rust implementations of "typical" practical problems is rather higher than it is for C++. And it seems to be getting steadily worse from my perspective on the outside.
As someone that participated in that conversation, I think that's a pretty inaccurate characterization of it. It's not about when the writer learned the language, but rather, what problem you're trying to solve. If you'll allow me to summarize very briefly (perhaps at the expense of 100% accurary):
* Use unwrap/expect when you don't care.
* Use `try!`/`?` with Box<Error> in simple CLI applications.
* Use `try!`/`?` with a custom error type and From impls in libraries.
* Use combinators (e.g., map_err) when you need more explicit control.
You might imagine that you could use any number of these strategies depending on what you're trying to do, which might range from "a short script for personal use" to "production grade reliability."All of this stuff was available at Rust 1.0. (Except for `?`, which is today an alias to `try!`.) It all falls out of the same fundamental building blocks: an `Error` trait with appropriate `From` impls.
The one exception to this is that, recently, there has been a surge in use of crates like error-chain to cut down on the code you need to write for defining custom error types and their corresponding `From` impls. But it's still all built on the same fundamental building blocks.
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.
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.
I code in Rust and C++ every day, and am mostly equally experienced in both (maybe more Rust now, but this wasn't always the case). I disagree. Rust has some ergonomics issues that C++ does not, but the reverse is true too. Looking at rust from C++ you'll only see one and not the other, because you're used to the other.
(And as burntsushi said your characterization of the thread is inaccurate, people are suggesting things that are best for different use cases)
Rust hasn't really tried "hard" to make error handling simple. We have what we had during 1.0, and then we have the ? operator, which always existed as try!().
Other than the enforcement by the compiler, this is common in scala and f#, two other languages where raising errors is common.
Railway-oriented-programming is nice because it typechecks, and because it reduces cyclomatic complexity by short-circuiting without forcing you to handle the error until the end of the chain. Now, it is unfamiliar to those outside the FP community, but much of the rust language seems to be unfamiliar mixes of FP and low-level optimisation techniques.
With the Either type you don't even need to do that, but then errors on the left becomes convention instead of an enforced habit. The only odd part is the try macro. That should wrap the Val in a right, to preserve type information. I guess I could be convinced that Either = Error | Id [A].
No, ".unwrap" turns input errors into bugs, and there is no production code where this is more desirable than an exception.
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.
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.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 :/
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)
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.
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.
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"
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.
What do you mean about industrial processing? PLCs?
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.
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.
In an ideal language, you could decide to ignore low-level constraints and your code would work just fine, although perhaps less efficiently.
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.
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:
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' :-(
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.
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.
But you can do that in Rust, just use Box<Error>. No library does that though, because it is not a zero-cost abstraction.
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.
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.
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.
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 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.
Anyway for me exceptions are way easier to work with, than return codes.
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.
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.
"Generics may well be added at some point. We don't feel an urgency for them, although we understand some programmers do."[1]
Here are some links you may find useful if you decide to try again:
Documentation index: https://nim-lang.org/docs/theindex.html (everything)
Official Tutorial: https://nim-lang.org/docs/tut1.html
Standard library documentation: https://nim-lang.org/docs/lib.html
Language Manual: https://nim-lang.org/docs/manual.html (information on syntax, type system, GC, etc.)
Compiler user guide: https://nim-lang.org/docs/nimc.html
Backend docs: https://nim-lang.org/docs/backends.html (nim cmpiles to C, C++ and js)
Built in templating docs: https://nim-lang.org/docs/filters.html
How to tune the GC: https://nim-lang.org/docs/gc.html
Development tools docs: https://nim-lang.org/docs/tools.html
More about the compiler: https://nim-lang.org/docs/intern.html
All of these links come from nim-lang.org's documentation page here: https://nim-lang.org/documentation.html
I'm not sure why you put 'safe' in quotes here; nothing about 'unwrap()' (or even 'panic') is unsafe in the context of Rust. In fact, it acts just like Python would in the same circumstances: print a developer-centric message out and exit with a bad return code.
What's unsafe about that?
> if you're gonna have unwrap, you basically have to have exceptions as well
Why do you think that? unwrap() is meant to be the same as throwing an uncatchable exception; if you want to throw an exception that you mean to catch somewhere, you should be using something else.
> people prefer to use [unwrap()] than use the verbose match statement
People may not be aware (which will come with time) but there are more than just those two choices when it comes to error handling in Rust.
Unwrap is a shortcut to let you be lazy; it exists for no other reason, and it causes application level crashes in way that is very much easier to avoid in other languages.
That 'catch_unwind' exists is evidence that some kind of panic recovery is necessary... and I wonder how often you hit it from a real panic, vs. a stray lazy unwrap?
Whats your justification for unwrap? I've never seen a meaningful justification for it other than not wanting to handle errors properly.
An application error (returned null) shouldn't abort your application with a hard error, no logs. Its just plain poor practice to use unwrap().
It might crash, but it doesn't burn, which is kinda the point of panic. Its behavior is well defined and predictable, which is great improvement over typical C UB.
But speaking about blogs... Why not to use .unwrap() there? Its simple, and allows to show some ideas without digging into error handling, just point to places where those handling should be placed.
> but if you're gonna have unwrap, you basically have to have exceptions as well (or some kind of panic recovery) ...
https://doc.rust-lang.org/1.9.0/std/panic/fn.catch_unwind.ht...
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.
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#...
https://ocaml.org/learn/books.html
https://wiki.haskell.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.
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.
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.
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.
If you are not being hyperbolic then please backup your 90% claim.
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.They are still superior for error handling, exceptions would have a more complex interaction with them.
Er, well I was also using it as a mousepad for about three years...
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.
Simplicity is under-rated. With Nim, most problems are easy to grasp just by reading source code. And when I don't understand the docs, I look for a simple example at http://rosettacode.org/
Exception obfuscation frameworks (Spring) just kick the can down the road, creating problems without any discernible benefit.
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.
This would be similar to how Go functions always return the same error type, but without the boilerplate.
> 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.
We have `expect` it's like unwrap(), but takes a string. When the program encounters some error, it will exit and print the string.
Java has an exception hierarchy and exceptions as part of method signatures. If anything, Python's exception hierarchy started getting sorted out relatively recently.
Sorry I can't be more specific.
I'm sure your original question was more nuanced, but for anyone wondering about this basically any ref type is heap allocated, and you do this by calling new, so:
type MyType = ref object
var myHeapObject: MyType
new myHeapObject
Pretty much anything else is stack allocated and doesn't need the GC if I remember right.[0] https://nim-lang.org [1] https://nim-lang.org/docs/gc.html
.beware("could not open file"); .be_wary_of("but hole")Edit: 'or_panic(msg)' would be shorter and also good.
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...
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.
Your phrasing implied that you said the crash was not safe, as you put "unsafe" in quotes and contrasted it with the crash. At least, that's what I understood you to be saying too.
I feel like any time someone mentions the word 'safe' regardless of context, the rust safety pedants roll out of the woodwork to dispute to dispute any minute detail of what's been said, regardless of if its relevant to the discussion at hand. I shouldn't have put 'safe' in the comment at all, what a waste of a thread.
My point had nothing to do with safety; it was purely that having given advice being to write good code that doesn't panic, and then having code that shamelessly panics in all your examples is hypocritical.
`?` is a better choice in basically every case; I'm glad to see the documentation will be moving eventually towards using that.
Panics mostly match the verbosity, ergonomics, and functionality of the analogous python script.
Now I agree that a caveat should follow advice like using unwrap and expect, perhaps a small blurb about how they should eschewed for better error handling when you want to catch the errors and make decisions because of them (especially when writing libraries) but that's quite a bit short of hypocritical to me.
What? That's what every other language does.
file.open("foo").read_line()
If open fails it'll either throw or return null which will then cause read_line to throw a null pointer exception. Rust just makes things explicit here.Though it might be interesting if there was a special opt-in Deref impl for Result and Option so people could omit the unwrap and just get it implicitly, for the occasions when you don't want that explicitness.
Unwrap does what just about every other language with exceptions-by-default does: it prints out a message for developers and exits. If you don't want that behavior, that's fine; in Rust you'd not use unwrap(), and in other languages you'd catch the exception.
Now, I agree that if you are using a library which does unwrap() in Rust vs a library that throws an exception in Python, you have different situations. But unwrap() isn't meant to be used in libraries (or if it is, only for fatal errors which should be uncatchable).
> Whats (sic) your justification for unwrap? I've never seen a meaningful justification for it other than not wanting to handle errors properly.
That is the justification for it: you are writing a simple short script-like tool (or you are prototyping or exploring some problem through one-off or throw-away code) and you want any errors to immediately exit with a developer-centric message and failure code. unwrap() is perfect for this.
It's the same justification for not catching every exception in other languages. Sometimes you are fine with an error printing a message and exiting.
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?
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.
> 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
Your debugging work in this case might have also been alleviated by a good unit test covering this functionality.
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.
For example, in my production Rust code, i deal with all errors in reading and parsing config files with .unwrap() or .expect(). If a program cannot read its config file at startup, it cannot correctly do its job, and so the only correct thing for it to do is to abort.
Have i misunderstood what you were trying to say?
Take for instance a web framework like Django that responds with a 500 error page with a stack trace when an exception occurs.
There are interesting problems to be solved in the space of error handling, e.g. error handling in asynchronous code. But Rust is not even matching the state of the art achieved by Lisp and Python decades ago.
unwrap()'s behaviour is essentially the same thing as an uncaught exception in Python, only you can actually check for where they occur in the code with a simple grep rather than hoping your test suite caught every possible failure case.
1. If you're prototyping or writing a quick throwaway program, then unwrap will neither pick your pocket nor break your leg.
2. If you have an invariant that you either can't (or won't) move into the type system, then unwrap/expect/panic'ing can be appropriate precisely because if that unwrap gets tripped, then that will indicate a bug in your program that should be fixed.
The more succinct advice that I like to use is this: "if end users of your Rust application see a panic, then you have a bug." But this is slightly harder advice to follow because it's a statement about the user experience.
Absolutely but using the same mechanism (unwrap/panic) for both types of errors - recoverable and recoverable - can creates confusion. panic'ing for wrong user input for example as can be seen in example code.
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.
I think twenty-five years ago there were similar hopes and dreams for Java checked exceptions.
This is not a problem in Rust.
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.
That said, dict also has get(), which lets you specify the default value if key wasn't found (and the default default is None).
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.
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?
.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.
->
"an `Error` trait with appropriate `From` impls."
Note, that it may be entirely necessary for us to invent new idioms to make progress in the art of programming.
They're all unique in what they do. I'd still say a simple procedural language like C is easiest to read, but 'modern c++' has more idioms and quirks than Rust, in my experience.
And Rust has the advantage that the ecosystem is very well tied together and it has a great community and documentation.
#[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.
- "An Error trait" means that when you define a new type that will store error information, you have to define how it implements the Error interface.
- "appropriate From impls"... you are trying to wrap a number of error types in your own special error type, you need to tell the compiler how to convert another specific type into your type new type. There is an interface (trait) in the standard library for this purpose called "From". This is done as an alternative to inheritance in an error system. The trait signature looks like this:
trait From<T> {
fn from(T) -> Self;
}Why does every library implement its own Error type? It feels like reinventing the wheel, and it takes a lot of boilerplate code.
If you need to distinguish different kinds of errors, why not, for example, have a lot of useful pre-defined error types like Python does?
One takeaway I got from playing around with Rust (and using GitHub code search to work through my issues) was that coding style will probably differ considerably from project to project, which is very C/C++ like and maybe a good thing for the language, but I found a little disappointing.
That is literally the definition of hypocrisy; the behavior of people who do things that they tell other people not to do.
"When you do this, do it like this, but properly with error handling." :P
Anyhow, as I said its my oppinion that unwrap() is lazy, and `?`, `expect` and `assert!` cover the same functionality in more explicit and meaningful way.
You're welcome to your own opinion.
Except I'm not saying that at all.
I said it's fine to use unwrap() or expect() if you want script-like default behavior (a developer-centric error message and a quick exit with a bad return code) and if you want something more than that, then use something better than unwrap() or expect().
There's no hypocrisy here. I think anyone should follow that advice. Me, you, a newb to Rust, a Rust veteran, anyone. Same advice.
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.
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?
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.
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.
> 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.
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.
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.
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.
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.
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.
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.
Show me a compiler that's used in production which handles multi-inheritance exceptions without allocating extra data and I'll be happy to eat my words :).
Language != Implementation.
If the language would be "In GCC the thing X happens when ...", then ok.
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.
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.htmlSince 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.
The benefit of lib-specific error types, though, is that they can be far more specific.
Now, in most cases, callers don't care at all about which specific error happened. They just want to print the error and be done with it. And that works just fine, because of the error handling machinery. But if you do want to drill down, then the option is there for you.
And I understand that. Hence why unwrap everywhere is harmful.
> Absolutely but using the same mechanism (unwrap/panic) for both types of errors - recoverable and recoverable
Nobody is using panics with the intent to recover, in the classic sense of "recoverable error".
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?
> 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.
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.
Glad we're all on the same page though haha.
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.
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.
error_chain! {
foreign_links {
Io(io::Error);
Parse(ParseIntError);
}
}
and it'll even generate an aliased `Result` type as well as fancy chaining support.Parsing errors are recoverable as a class—you haven't irretrievably corrupted your process memory when you encounter one. Therefore, the parser itself should not panic(); it should just return an option type.
Parsing errors may very well be unrecoverable in a particular instance. The code calling the parser has every right to decide to unwrap() the option type such that a panic() will happen if the parse failed. The code calling the parser is likely business-logic code of an application, and is privy to knowledge like "if there is no configuration supplied here, then later code that tries to consume the configuration will have to crash" and so can decide to early-exit with a user-comprehensible error ("you don't have a config file!") rather than letting the later code crash with some weird error about a config value being Nothing.
.unwrap is only suitable for the unrecoverable case, while exceptions are suitable for both cases. Hence why .unwrap in examples is harmful imho.