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.
What I like better about Rust, and what I think most people are actually complaining about with Go, is that syntactic sugar like the ? operator and functions like unwrap(). It’s a lot more concise and your application logic doesn’t get lost in verbose error checking code.
That's kind of the point. The type system should be powerful enough to disallow those cases then.
In practice, I've seen both, always accidentally. I've also (more commonly) seen a lot of confusion and annoyance around:
Okay, so this has to return a pointer for the error case, should the caller check that? If not, how do we square that with checking for nil pointers being generally a pretty good rule? If we do check, our unit test coverage has a blemish for every call since nothing can hit that. If we skip it being a pointer, then it's a zombie object.
It's just a lot of cognitive load and bikeshedding around an issue that shouldn't exist.
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.
> 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.
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.
I'm not sure one is better than the other, just different tradeoffs.
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.
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.
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.