5 ms·
The Result approach believes that the producer knows what is best for the caller regardless of who the caller is. The Go approach believes that the producer sho
by randomdata 3y ago
The Result approach believes that the producer knows what is best for the caller regardless of who the caller is. The Go approach believes that the producer shouldn't assume it knows the caller.
I'm not sure one is better than the other, just different tradeoffs.
- iudqnolq 3y agoDoesn't the producer know best whether the producer failed?
- randomdata 3y agoDoes the caller care? By day I work with a team in a language that sees errors ride on the exception handling system. Staying within the original example, I see code like this all the time (too often, even, but that's another topic for another day): try { file = getFile() } catch(/* ... */) { fileUnavailable() } Here, the assumption of getFile that the caller wanted an error was incorrect. A Result-using language would end up in a similar place. Idiomatic Go says leave it to the caller. Like above, when only wants to know if there is "file or no file" without concern for why there is no file, then: file, _ := getFile() // The second return argument is an error. if file == nil { fileUnavailable() } I doubt either way makes much difference in this contrived example, but the difference shows up when it extends out into real code. There are plusses and minuses to each way of seeing the world. Tradeoffs, as always.
- nulld3v 3y agoBut in Rust you can do the same, I do this all the time: let _ = fs::mkdir_all() // Error ignored, Rust will not complain because you explicitly assigned to _ Or if the function returns something I need but I don't care about the error: let Ok(file) = get_file() else { file_unavailable(); return } upload_file(file); Or this: let file = get_file().unwrap_or_default()
- randomdata 3y ago[flagged]
- nulld3v 3y agoSo what is the point of the distinction then? The examples I gave were idomatic Rust too. Both Go and Rust hand the caller an error and the caller then must do something with the error. In both Go and Rust, you can assign the error to underscore and ignore it. Java is different but we aren't talking about Java right now. The only difference is that in Go the function can (and must) still provide a value for "file" even when there is an error. In Rust you can kind of do something similar, by giving the File struct a default value (that's what "unwrap_or_default()" is) but it isn't exactly the same. I would argue that such a case is extremely rare though.
- randomdata 3y ago> Both Go and Rust hand the caller an error and the caller then must do something with the error. This is simply not true. Go preaches that values should always be useful. Given a return signature (T, error), the value of type T can be used irrespective of what is contained in the value of type error. They are independent states. > The only difference is that Go provides a value for "file" when there is an error. Yes, that is ultimately the difference. Result takes what are independent states and tries to make them dependent. Result<T, error> assumes that if there is a value of type error then the caller would not make use of the value of type T. Often they will be dependent for all practical purposes. It is not an unreasonable assumption to assume that they are, especially if you know the caller. However, Go believes you cannot make that assumption. That you have to let the caller determine what it finds useful, not what you think it might find useful. Ease of code reusability is clearly a design goal of Go, so the idea that you can't get to know your callers and how they will use your function isn't unwarranted. Tradeoffs, as always.
- awused 3y agoResult covers the very common case where there is T or error but never both. Having a decent type system doesn't prevent you from returning both. In Go I simply cannot have something as simple as "A or B, but never both or neither" as a type. >However, Go believes you cannot make that assumption. This is trying to twist a weakness of Go's type system as a virtue. In Go I always have "A or B, including neither and both". I can have that in Rust or Haskell if I want, but usually I want "A or B, never both or neither" and rarely "A or B, sometimes both but never neither" which Rust and Haskell can do but Go cannot. This comes from Go using product types as a poor replacement for sum types.
- earthboundkid 3y agoYou mean file == nil.
- simiones 3y ago> Idiomatic Go says leave it to the caller. Like above, when only wants to know if there is "file or no file" without concern for why there is no file, then: > file, _ := getFile() // The second return argument is an error. if file == nil { fileUnavailable() } That very often doesn't work in Go. Most functions which return errors offer no guarantee whatsoever about the return value if there is an error. No one would consider it a breaking change to modify the return in case of error. And many functions return a struct, where Go offers no way to compare two arbitrary struct values for equality, or to check if an arbitrary struct value is that struct's "zero value". So no, Go does not recommend (or even endorse) this pattern.
- kadoban 3y agoThis doesn't really make much sense. The producer knows what it's returning. In the _vastly_ common case, it's either returning an error or a success object, but the Go type system is unable to represent that. The caller trying to pretend that the success object is there isn't a freedom the caller gets in the current system, it's an artifact of the type system not being powerful enough to encode the situation accurately. In practice (for the success object) it means you need to check for a nil pointer, make sure you don't use a zombie object, or just rely on an assumption that it's not nil, depending on which poor choice the producer function went for. If you have a function that can return both an object and an error, there still should be a way to represent that (exactly the current way). Having Sum types would just allow a way to represent the common case accurately.
- randomdata 3y ago> The producer knows what it's returning. But doesn't know how the return values will be used by the caller. What is perhaps lost in this is where Go says that values should always be useful? > If you have a function that can return both an object and an error, there still should be a way to represent that (exactly the current way). Exactly the current way is what is said to be deficient, though. A function of this type is naturally going to return a file every time because a file is always useful, even when there is failure. Whereas Result assumes that you won't find the file useful when there is failure. If you know the callers you can discuss if the file will ever be useful to the callers who use it under failure condition. Always useful does not mean always used. But Go, no doubt of a product of Google's organizational structure, believes that you cannot get to know your callers. You have to give them what you've got and let them decide what and what isn't important to their specific needs. Tradeoffs, as always.
- awused 3y agoThis tries to sound profound but it really just misunderstands basic sum types. In Rust/Haskell/etc you're free to return multiple values if that's even potentially useful. If you have a function that can completely fail, partially fail (in this example, opening a file but then something else fails), or entirely succeed, you can encode that in the type system. In Go you cannot define this in the type system, you just have comments explaining how it works and hoping both that you implement it correctly and callers of your code read and understand them. If your code does return both files and errors in some cases, chances are callers are going to handle it incorrectly, especially if, say, the caller is responsible for closing files/responses/streams/etc. In Rust/Haskell I can write a type with three values like Success(file), PartialSuccess(file, error), or Failure(error). Callers must then handle all of these. In Go I always have four cases for a simple function including those three and the final case of neither file nor error. Most Go callers will not handle the case where err != nil and file != nil and often the case where err == nil and file == nil will cause a panic and crash the program. There was a tradeoff for this, but in this case the tradeoff was entirely in making the Go compiler simpler at the cost of making the Go language weaker and Go code more error-prone.