Hacker News new | ask | show | jobs
by kosherhurricane 1047 days ago
> If a programming language supports union types and type inference then there is no need for such a "special" language feature

Laughs in Go.

> While Zig's errorhandling is not bad and miles above Go's

Laughs again in Go. Go has no "foundations" regarding errors, it's just a convention. It has no union types. It doesn't have weird corner cases. It's just a returned value you can handle. Or not.

Of all the error handling paradigms I've seen, Go's requires the least amount of "specialized thinking" (try/catch or whatnot)--it's just becomes another variable.

2 comments

Go's lack of sum types mean that there is no static check for whether the error has actually been handled or not. Go's designers went to all the trouble of having a static type system, but then failed to properly reap the benefits. Sum types are the mathematical dual of product types. It makes sense for any modern language to include them.
Ever since I learned of sum types, they have ruined my enjoyment of programming languages which don't have them. I sorely miss them in C++ for example (and std::variant is not a worthy alternative). I don't understand why any new language wouldn't have them.
Pedantic typechecking is like learning to spot improper kerning, you think it’s a good thing but you spend your entire life cringing at the world around you.
std::variant is a good example of many things bad with c++ improvement process, as a language.

If you want to just pattern match on type of visitor there is “another convenience helper” that you need to bring, and result still looks not pleasant.

Introduced in like c++17, even in c++23 you still need to write a std::visit to process it. Committee members waste time on yak shaving that std::print

Just wait until you learn union types, type classes, type providers, and so on. It's even worse afterwards. :-)
In theory the lack of sum types sounds like a drawback for Go error handling, in practice it does not matter at all IMO.

So far I have never worked a Go project without a strict linter enabled on the pipeline checking that you handled the case when err != nil. I don't care if it is the compiler or the linter doing it, the end result in practice is that there actually is no chance of forgetting to check the error, and works just as well as a stronger type system while also making the code stupidly obvious to read.

> no chance of forgetting to check the error, and works just as well as a stronger type system

A linter-based syntactic check is no substitute for a proper type system. A type system gives a machine checked proof. A heuristic catches some but not all failures to handle errors, it will also give false positives.

Error handling via sum types only enforces the rather weak constraint that you cannot access a non-error return value in the case where the function returns an error. It certainly doesn’t catch all failures to handle errors. For example, in Rust you are perfectly free to call a function which returns a Result and then ignore its return value (hence ignoring the case where an error occurred). Go’s error checking linters impose stricter constraints in some respects than the constraints on error handling imposed by Rust’s type system.
> only enforces the rather weak constraint that you cannot access a non-error return value in the case where the function returns an error.

This "rather weak constraint" as you put it, completely solves Tony Hoare's "billion dollar mistake": null pointer exceptions. Something Go also suffers from due to lack of Sum types. With regard to your Rust example, the compiler will give a warning that can be turned into an error to completely prevent this, if desired.

As the parent said, sum types are "foundational" and have many applications for writing safe statically checked code. Eradicating null pointers and enabling chainable result types are only the tip of the iceberg.

> This "rather weak constraint" as you put it, completely solves Tony Hoare's "billion dollar mistake": null pointer exceptions.

Yes. However, it doesn’t do what you seemed to be suggesting that it does, which is “catch all failures to handle errors”. You correctly note that Go’s linters can’t always do this, but also seem to erroneously suggest that Rust’s type system somehow can. This is backwards. Go’s error linters catch most instances of ignored error values, whereas Rust’s type system doesn’t do anything, in and of itself, to ensure that error values are not ignored. Of course there are compiler warnings to catch unused errors in Rust, but that’s fundamentally the same thing as the warnings you get from Go linters, and has nothing really to do with sum types. Whether or not an error is ‘ignored’ or ‘used’ in any interesting sense is not a formal property of a program that can be formally verified. (Yes, linear types, etc. etc., but you can formally use an error value in that sense while in practice ignoring the error.)

By the way, you don’t have to preach to me (or really anyone on HN) about the virtues of sum types. I’ve written a fair amount of Haskell and Rust code. My issue here is not with the utility of sum types, but with the erroneous claim that they somehow remove the need for linters or compiler warnings that flag unhandled errors.

Rust compiler warns about ignoring a result of functions annotated with #[must_use]. This is optional because if a function has no side effects, then ignoring its return value is not a problem and shouldn't be warned about.
Yes, but that’s just the sort of linting you can also get vía Go tools. It’s not something that’s possible because of sum types.

> if a function has no side effects

A property which of course is not tracked by Rust’s type system. My only point here is that neither sum types in general nor Rust’s specific implementation of them provide any means of ensuring that errors are handled. They do other nice things, just not that.

> no chance of forgetting to check the error

I remember a similar discussion here a couple months ago and some guy linking numerous open bug reports in Docker's repo with this very issue.

No language has a 'static check for whether the error has actually been handled or not'. In Rust, for example, you can just 'unwrap' an error. In Haskell you can use 'fromJust'. And in Go you can just ignore 'err' and assume it is 'nil'.

Sum types might be the 'mathematical dual of product types' but programming languages are not mathematics. The possibly implementations of sum types are quite varied. It makes sense in low-level languages for the programmer to use what makes sense in the particular situation.

Unwrap and fromJust can be disallowed if need be, they are "unsafe" convenience functions whose use can and should be tracked. Not all languages with sum types will permit them. Rust also has "unsafe" code blocks, should we also claim it is therefore not memory safe? Some would try to do so, but at least this unsafe code is tracked and not idiomatic.

> programming languages are not mathematics

This may be how you choose to view them. But many of us seeking to build safer and more correct software aim to make programming more like mathematics. Mathematics tells us how to compose and tells us how to prove. Both things the software industry is currently failing at.

>Unwrap and fromJust can be disallowed if need be, they are "unsafe" convenience functions whose use can and should be tracked.

And with the same sort of third-party tools you use to 'track' those and ensure they're not used, you can track unused error returns in Go or C.

> Not all languages with sum types will permit them.

All do.

>Rust also has "unsafe" code blocks, should we also claim it is therefore not memory safe? Some would try to do so, but at least this unsafe code is tracked and not idiomatic.

Absolutely we should! Rust fanatics try to claim it is a memory-safe language when it isn't. Real memory-safe languages like Java have existed for far longer.

>This may be how you choose to view them. But many of us seeking to build safer and more correct software aim to make programming more like mathematics. Mathematics tells us how to compose and tells us how to prove. Both things the software industry is currently failing at.

You missed the entire point of what I said.

> All do

No, this is not true. A total functional programming language can disallow partial functions that circumvent the type checker.

Again, linters only get you so far. For example, sum types eradicate null pointer exceptions, linters do not.

I don't give a shit what languages CAN do in theory.

I care what languages DO do. And all languages (that aren't research projects like agda) provide escape hatches here. Even Haskell.

>Again, linters only get you so far

You brought up linters. Not me.

A good point. Newer languages influenced by Go, like Vlang[1], have sum types partially for such reasons.

[1]: https://github.com/vlang/v/blob/master/doc/docs.md#sum-types

> Go's lack of sum types mean that there is no static check for whether the error has actually been handled or not.

I dunno, my IntelliJ calls out unhandled errors. I imagine go-vet does as well.

A simple syntactic check will only ever work as a heuristic. Heuristics don't work for all cases and can be noisy. The point is, no modern language should need such hacks. This problem was completely solved in the 70s with sum types.
Cries in go. I segfaulted go while learning it in the first 5 minutes. Its a a solved problem, unfit for general purpose programming on this problem class alone