Hacker News new | ask | show | jobs
by grumpyprole 1048 days ago
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.
5 comments

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.

> but also seem to erroneously suggest that Rust’s type system somehow can.

I didn't bring Rust into this discussion, it is hardly a model implementation of sum types and using them for proofs, but it is certainly a step in the right direction.

> 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

You are misrepresenting my posts, I am responding to an erroneous claim that linters are a satisfactory substitute for sum types.

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.

He still has a point. In theory you might need a language like Idris/Agda, but in praxis it still makes a big difference.

It is true that you will see that a function can return an error and that you choose to ignore it. It's also true that you can do the same in many other languages that use sumtypes.

But it is still different. Because while ignoring an error in go is as easy as putting an underscore next to the happy-case, in languages with sumtypes that doesn't work.

The equivalent in other languages would be to return a struct and then just access one value and ignore the other one. In that case, the practical implications would be the same.

But when using a sumtype, a few things change.

First, you can not just access the happy-case value, you are (or at least can be) forced to also "access" the unhappy-case value. Either in a pattern match, in a fold-function and so on.

You know have to return something, even if it is an empty value our "escaping" by throwing an error.

On top of that, what happens if a function can partially succeed? Take a graphql request as a practical example where this is quite common.

With Go's style of error handling, how do you model that? I.e. say you need to redactor a function that previously either succeeded or failed into one that now can partially succeed and fail.

In a language with sumtypes I would now switch from a sumtype Success|Error to more complex type Success|Error|PartialSuccess which makes it a breeze to refactor my code because the compiler will tell me all the places where I have to consider a new case and what it is.

I'm genuinely curious, how would you model that in Go and what implication would such a refactoring have on existing code?

I imagine it would be quite different in praxis.

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

Then I'm sure you'll be very satisfied with languages like Go and Zig. I am not satisfied with them, because I know what can be done.

> You brought up linters. Not me.

I did not. They were brought up by others as apparently one way that Go programmers work around lack of sum types.

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.