I'll note that The Fine Article is about why one of those established idioms confuses newcomers. Even better, it bites people because the type of an interface needs to be nil for something to work. (Insert appropriate expression of astonishment here.)
Enforcing patterns hiding inadequacies in the implementation works less well than one would imagine, e.g. look at all the languages that Go sought to improve upon and the baggage of "design patterns", GoF-alikes, etc. that they brought with them.
result, err := Foo1()
if not err {
Panic(err)
}
result2, err2: = Foo2(result1)
if not err2 {
Panic(err2)
}
This might allow you to skip the nesting of blocks, but why would you do this when multiple languages exist where you don't have to thread around error messages everywhere? For instance,
result1 <- foo1
result2 <- foo2 result1
(Yes, that's Haskell syntax, but you can do things with a similar lack of pain/verbosity/error-prone-ness in many languages.)
I used to fight Go's error system by creating wrapper functions to mitigate the need for nested blocks - essentially trying to "Haskellify" the code a little (for want a _very_ crude description). With time I realised I was spending more time over thinking a solutions and pontificating instead of just writing code and handling errors. Since then I've given up fighting against the language idioms I personally disagreed with and as a result I've come to appreciate some of the benefits they came with but I previously had overlooked. Ok `err != nil` is about as ugly as it comes, but it's still effective in getting the job done.
At the end of the day it doesn't make any more sense to apply Haskell methodologies to Go than it does to complain that Haskell is missing some fundamental features of Go. They're distinctly different languages. But despite this I've noticed you spend a lot of time in various Go discussions on HN moaning that Go isn't more like Haskell.
I use Haskell examples because that's the language I'm most comfortable with, but, e.g. the Result type used in Rust is another example of how this can be done better.
Ergonomic error handling or generics/parametric polymorphism aren't "Haskell methodologies". Go is one of a very small number of languages that have been designed in the last decade and lack features like this.
The reason I participate in HN comment threads about Go is largely how entertaining I find comments strenuously rationalizing Go's inadequacies. In one (very recent but definitely memorable) case[1], I was told that
> You [should] first reconsider your need to make a generic data structure and evaluate on a case by case basis.
Rust is my favourite language, I beg to disagree that its Result or Option types are more concise or require less boilerplate. The only real differences are that Rust's are type checked and harder to use.
Sure, in an unrealistic subset of cases, try! can hide the mess. But these aren't fair, and especially nor is your comparison.
Any practical code using Results soon ends up wanting to mix the errors from multiple sources. This requires a lot of boilerplate effort to make everything interop, and the machinery to reduce this is both complex and not standardised. If you don't go the upfront boilerplate-and-machinery route, things look awful.
And of course, if you use something else, like an Option, you're back to
let foo = match bar() {
Some(foo) => foo,
None => return None,
};
Go is much more consistent, and less pathological.
Your example is especially disingenuous, though. For example, you chastise Go with
defer file.Close() // BUG!! this returns an error, but since we defer it, it is not going to be handled
but ignore the fact that this "bug" is nonoptionally hardcoded[1] into the Rust program. Which is it then?
Rust's error handling looks nice on fake examples, and manageable inside self-contained libraries. My experience of actually using multiple libraries is that Rusts error handling is a choice between tangly handling of nested errors or verbose attempts to early-exit.
I didn't say ergonomic error handling nor generics et al were Haskell methodologies. I'm saying you keep coming into Go threads just to troll that Go isn't as good as Haskell. This isn't even the first thread this week you've been making Haskell vs Go comparisons.
Again, at the risk of repeating myself, it's not Haskell, it's "many languages other than Go, of which Haskell is one I'll be using by way of example".
(I'm not sure why you made that edit, but I've made a mental note of what the last bit said. Arguing on the internet is a difficult, if useless, skill, and I'd hate to be tiresome.)
1. Your assumption is wrong, I definitely didn't mean panic.
2. I think the arrow means assignment in Haskell and you are just referring to monadic errors? To use them the same way proper error handling is done in Go, you would just have more nesting and multiple unwraps, which is marginally different than Go syntax (but definitely with more compiler checking.)
The arrow syntax in Go is used by channels.
Apologies. In any case, monadic errors in Haskell allow you to make "failing early" automatic. Even a simple use of optional types can make a difference. For instance, here you're failing with the same error every time, like here:
a, err := squareRoot(x)
if err { handle(err) }
b, err := log(a)
if err { handle(err) }
c, err := log(b)
if err { handle(err) }
you can just use an optional ("Maybe") type: write a, b, and c with types like
a :: Double -> Maybe Double
and then do
a <- squareRoot x
b <- log a
c <- log b
If the computation of a fails, the whole computation fails. The compiler takes care of all the error-checking plumbing. I think the ergonomics of this common kind of situation are really suboptimal in Go, which to my knowledge doesn't support anything remotely similar.
That's right. But I prefer to decorate each error as it comes back from the callee, writing what I was trying to do that failed. This gives a human readable trace of the problem, and also a unique signature for the error itself.
> But I prefer to decorate each error as it comes back from the callee
That's trivially feasible and still shorter than the Go version:
a <- decorate (squareRoot x)
b <- decorate (log a)
c <- decorate (log b)
Outside of the do context, your return value is just that, a value, you can manipulate it using the language's regular tooling. And you can decorate the do context itself if you want the same decoration for all calls in the block.
I'll note that The Fine Article is about why one of those established idioms confuses newcomers. Even better, it bites people because the type of an interface needs to be nil for something to work. (Insert appropriate expression of astonishment here.)
Enforcing patterns hiding inadequacies in the implementation works less well than one would imagine, e.g. look at all the languages that Go sought to improve upon and the baggage of "design patterns", GoF-alikes, etc. that they brought with them.
> return early
I was just going off what I found at https://blog.golang.org/error-handling-and-go.
I assume you meant this (in Go-ish pseudocode)?
This might allow you to skip the nesting of blocks, but why would you do this when multiple languages exist where you don't have to thread around error messages everywhere? For instance, (Yes, that's Haskell syntax, but you can do things with a similar lack of pain/verbosity/error-prone-ness in many languages.)