7 ms·
One thing I don't get (and would honestly appreciate if was explained to me) is how the Result monad differs significantly from Go's error handling, other than
by dlisboa 3y ago
One thing I don't get (and would honestly appreciate if was explained to me) is how the Result monad differs significantly from Go's error handling, other than being a "true" monad. Most Rust code I see does things like (from the docs):
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => // handle err
};
It isn't much different from:
file, err := os.Open("hello.txt")
if err != nil {
// handle error
}
I've come to appreciate Go's simple solution as you get pretty much 90% of what you want from an operation that might produce an error (either the value or an error), and the flow control aspect of it is more explicit than with exceptions.
Maybe I don't get Monads, but it seems pretty much equivalent for the common use case.
- lolinder 3y agoThe killer feature of Rust's Result isn't actually the monad itself, it's the ? operator. Being able to concisely say "if there's an error, bail out by returning it" gets you pretty close to exception-level convenience with just a bit more explicit syntax showing where an error might come from.
- preseinger 3y agoyou don't want exception-style "convenience", that's the whole point you want to be able to read code and see a single control flow ? subverts that core requirement
- nulld3v 3y agoThe problem with exceptions is that they can come from any line of code and cause a "return". Rust's question mark solves the issue because it marks which lines of code can cause a "return". Therefore, you can always see see the control flow of a function. Moreover, you can go even further if you really really really want a single control flow. You can write a clippy lint to disallow early returns (ban "return" keyword) and question marks. That said, I really don't think this is a good idea. The "return" keyword exists for a good reason.
- preseinger 3y ago? enables chaining, chaining subverts comprehensibility in exactly the ways i'm describing
- consilient 3y agoAll languages have chaining: that's what `;` is for. Chaining together transitions in an imperative state machine isn't simpler than chaining together `Result`s, you're just used to it.
- preseinger 3y agochaining means combining a sequence of expressions that each take the same input as they give as output ; doesn't do this, afaict when you're writing imperative code it's important that control flow (return) is explicitly visible
- consilient 3y ago> chaining means combining a sequence of expressions that each take the same input as they give as output > ; doesn't do this, afaict It's chaining together transitions in the state machine that is your program. > when you're writing imperative code it's important that control flow (return) is explicitly visible Control flow is visible with `?` or other do-notation variants. If I want to error out in a `Result` context, I explicitly return `Err(bad stuff)`. And if I don't, I explicitly return `Ok(return value)` instead. If I want to introduce a new asynchronous value in js, I explicitly call `new Promise`. And so on. What's not visible in a do-block is the implementation of control flow. Which is fine, because this isn't the code that controls it - `Ok(Err(x))` is reduced in exactly the same way no matter what `x` is or where it came from. Traditional imperative code is the same way: the runtime system always works the same way, no matter which statements you ask it to execute. If you do choose to expose the underlying mechanisms of your control flow everywhere, you get continuation-passing style, which is useful in small doses but more or less impossible to reason about at scale.
- kaba0 3y agoExcept that without try-catch blocks you will have n separate control flow instead of a trivial to see pattern.
- preseinger 3y agohuh? it's exactly the opposite ideally, control flow goes 'down' via func calls, and 'up' via return statements this is the "trivial to see pattern" -- the code as written exceptions subvert those simple rules, they say any expression can potentially be a return statement, and recursively so!
- bvrmn 3y agoAnd what the point to make `if err {return err}` in most of cases? I've read a lot of code and error handling logic almost never in caller location.
- kitd 3y agoI've not used Rust. Do you get to add context at the point of bailing?
- dthul 3y agoThat depends on the type you use to represent errors. A more "fancy" error type will allow you to add context / backtraces / whatever you want, while a simpler error type, for example just an error code, does not. (The error type is the E in Result<T, E>).
- oneshtein 3y agoNo, context is not added by default. With third party libraries like anyhow, it's possible to add context[0] before `?` operator in human-readable or machine-readable form: a_method().context("Failed to complete the work")?; a_method().context(FailedToCompleteTheWork)?; [0]: https://docs.rs/anyhow/latest/anyhow/trait.Context.html https://docs.rs/anyhow/latest/anyhow/trait.Context.html
- treyd 3y agoRust's error handling and option types actually aren't monads, they just have similar ergonomics for end users. There's some tricks that have to be done to make them work in a eager evaluation context, and as a result implementing iterator combinators does not feel like working with modads.
- mkehrt 3y agoIn what sense aren't they monads? They have a bind method ("and_then") and a return method (Which is just the variant for constructing the success case, i.e. "Ok" or "Some"). It's more idiomatic to use "map", but that's just a degenerate case of bind. > There's some tricks that have to be done to make them work in a eager evaluation context... Monads have nothing to do with laziness, though. In Haskell, IO actions are used with laziness and to work around purity, and IO actions form a monad. But that's just one instance of monad.
- treyd 3y agoAfter doing some research to refresh my memory I found this old thread: https://users.rust-lang.org/t/what-is-a-monad-and-who-needs-haskell-anyway/45710/7 https://users.rust-lang.org/t/what-is-a-monad-and-who-needs-... I believe what I had originally told that makes them not monads is that because Rust goes through some convlutions to fake the laziness of Haskell monads, it makes them not be typed like Haskell monads. For example, the declaration of the `.flat_map` on an iterator is actually `fn flat_map<U, F>(self, f: F) -> FlatMap<Self, U, F> where Self: Sized, U: IntoIterator, F: FnMut(Self::Item) -> U`, which uses a "do-er" struct instance of `FlatMap` and is itself another iterator. Evaluating the entire monad-ish iterator combinator chain with something like `.collect()` or `.last()` or something is what triggers the evaluation.
- kadoban 3y agoResult is better because it actually encodes the correct situation. You either get a file or an error. Not neither, not both. Go's encodes instead "you may or may not have a file" and "you may or may not have an error". Not the same thing, and extremely rarely what you want, IME. Other languages also do a better job of helping you verify that you actually handled both cases too. By the way I wouldn't say we need Monad here, I'd be happy if Go could at least encode Sum types.
- XorNot 3y agoThis seems like a distinction without a difference. In reality, in code, I'm still doing something like `if (!result.failed) { do the thing } else { do the other thing }` Like...this makes no difference to my ergonomics at all. In many cases it's arguably worse because now 1 token is potentially representing two very different types I want to deal with. The primary benefit to me seems like it's more to do with the ease of generic code handling: everything can be Results, and then I can evaluate them all to see if any of them are errors, which in turn makes failing out of many different operations easier - I'm not handling a file type, a string, some numbers etc.
- mkehrt 3y agoIn rust the type system enforces you check the error. In go, it doesn't. (Because rust error types are enums/sums and go error types are structs/products). This seems like a huge difference. > In many cases it's arguably worse because now 1 token is potentially representing two very different types I want to deal with. Yes, that's what an enum/sum type is? That's the whole point.
- philosopher1234 3y agoIn rust it is not possible to use incorrectly, and in go it is, sure. But whether it is possible or not is only one dimension. Does it matter that it’s possible to misuse errors in Go if it virtually never happens? I just don’t find the point about what is possible interesting. The other trade offs around readability, ergonomics, and so on seem more impactful.
- justinsaccount 3y agoThe biggest difference is in rust you have to handle the error case, but in go you can accidentally ignore it.
- philozzzozzz 3y agoNo compiler can force you to handle an error. Rust might force you to access a value. Big. Fucking. Deal.
- mkehrt 3y agoThe point isn't that it forces you to access the error. The point is that it prevents you from accessing an invalid return value.
- meling 3y agoI believe there is a vet check for this. So while the compiler won’t stop you from ignoring errors, it will tell you that you aren’t checking an error. Yes, you can ignore the error with _, but then it isn’t accidental.
- jppittma 3y agoThis seams silly and nitpicks to my. In go, you have to assign the error, and if you assign it you have to use the variable. I've never seen this mistake before in my years of using go.
- nulld3v 3y agoGoing to link my comment here: https://news.ycombinator.com/item?id=36398874 https://news.ycombinator.com/item?id=36398874
- masklinn 3y ago> In go, you have to assign the error, and if you assign it you have to use the variable. Nope. Go errors on dead variables, not on dead stores. And because idiomatic go tends to reassign errors to the same variable if you have multiple error-returning functions in the same scope any one of them being checked will make the compiler happy. You also do not have to assign the error at all, you can just ignore the entire thing.
- nemothekid 3y agoThe Rust code you have isn't idiomatic. It's gar more likely to see: let greeting_file = File::open("hello.txt")?l vs file, err := os.Open("hello.txt") if err != nil { return fmt.errorf("faild to open hello; %w, err) } However, I would argue the distinction isn't just cosmetic. The compiler prevents me from not checking the error and blowing up the application with a nil pointer exception. The TFA even capes this pattern: type Result[T any] struct { Value T Error error } and I wonder if we will start to see it more now that generics are in Go.
- philozzzozzz 3y agoWhere is the error handling in your Rust code?
- kinghajj 3y agoThe `?` at the end of the Rust example expands into the equivalent of the Go `if err != nil` idiom.
- philozzzozzz 3y agoYou are illustrating the point beautifully.
- dlisboa 3y agoNot a Rust programmer, but I took the Rust code from the Rust book. Admittedly I didn’t read the whole chapter, perhaps they talk idioms later.
- lolinder 3y agoYou just needed to go down a bit further [0]. They understandably didn't want to introduce syntactic sugar until after they'd introduced the syntax it desugars into. [0] https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator https://doc.rust-lang.org/book/ch09-02-recoverable-errors-wi...