
Show HN: A utility for running exhaustiveness checks on “sum types” in Go - burntsushi
https://github.com/BurntSushi/go-sumtype
======
wyc
Language-level sum types are something that I miss a lot when I'm writing Go.
I find myself using this error-prone pattern whenever I'm trying to represent
state:

    
    
        type PersonType string
    
        const (
            EmployeePersonType PersonType = "Employee"
            ManagerPersonType PersonType = "Manager"
            ClientPersonType PersonType = "Client"
        )
    

I'm not the only one[1]. Another option is to use iota.

    
    
        type PersonType int
    
        const (
            EmployeePersonType PersonType = iota
            ManagerPersonType
            ClientPersonType
        )
    

You'll then use generators such as stringer or jsonenums[2] to take care of
translating your internal representations to something more system-level and
printable. This gets very messy as now we need to write un/marshallers and
code generators for SQL databases, NoSQL databases, JSON endpoints, etc. I'm
likely to just keep doing what I'm doing.

Although my described approaches aren't great, I'm glad to see people thinking
about how to improve it with tooling. For reference, this is the same thing in
Haskell:

    
    
        data Person = Employee | Manager | Client
    

I really prefer this format because the set of possible Person type options is
explicitly defined and easy to exhaustively check. You'll get no type coercion
from arbitrary strings like "Daughter".

And you can also extra data types using type constructors:

    
    
        data ApptTime = NotScheduled | WalkIn | At UTCTime
    

At least by attempting to use sum types in Go, you get some compiler checks,
which are a lot better than none.

Has anyone figured out a better way?

[1]
[https://github.com/kubernetes/kubernetes/blob/master/pkg/api...](https://github.com/kubernetes/kubernetes/blob/master/pkg/api/types.go#L2860)

[2] [https://github.com/campoy/jsonenums](https://github.com/campoy/jsonenums)

~~~
aston
You may want to take a look at
[https://github.com/alvaroloes/enumer](https://github.com/alvaroloes/enumer)
which can generate functions around your enum to give/accept string
representations, plus JSON and SQL scanning.

------
grabcocque
Isn't trying to hack a language to have the type system you wish it had,
rather than simply using the language you wanted in the first place, something
of a futile exercise?

If you want a language with ADTs and exhaustive patterns out there, there's
plenty to choose from. Using a language with a weak type system and then
trying to wish that fact away seems pointless.

~~~
meddlepal
Not everybody gets to choose the language they want to use at work
unfortunately.

~~~
pjmlp
We get to choose, when there is freedom to choose to whom we work for.

Of course, changing jobs isn't always an option.

~~~
michaelmior
And someone can still love their job even if the language in common use isn't
their personal preference.

~~~
burntsushi
That's the case for me at least. I was even involved in choosing Go at our
company a few years back. A language doesn't need to be perfect in order to
enjoy using it. :-)

------
Animats
This is an old argument. If a language allows a switch statement where no case
being executed is an option, there's an opportunity for bugs. If a language
doesn't allow that, some people bitch about it being too pedantic.

A strong argument for insisting on covering all the cases (possibly with an
explicit default) is that someday, someone may add a new enum value, type, or
whatever, and code that fans out on that value may invisibly become invalid.

------
notheguyouthink
Cool! I didn't honestly expect to see more Go code from BurntSushi. Thought he
was a Rust convert :)

 _(not that someone can 't be both, of course)_

~~~
rkangel
> (not that someone can't be both, of course)

The Go vs Rust narrative seems to have thankfully mostly gone away (although
I'm sure it will never die out completely). The areas that they're targeted
and the things that they are best for have now become clearer to the community
at large, and they don't necessarily overlap that much.

That said, I don't use either of them at work, so I'd interested to hear a
counter-argument.

~~~
geodel
> The Go vs Rust narrative seems to have thankfully mostly gone away

I am not aware of that. Has something changed recently? Though IMO Rust's
strength could lead interesting usage in Data processing/analytics platform
like Hadoop/Spark etc as opposed to Go like web services.

~~~
kibwen
"Go vs. Rust" was initially only a thing because 1) Google initially
positioned Go as a systems programming language, 2) early prototypes of Rust
were very Go-like (with both a garbage collector and segmented stacks), and 3)
web developers who were already used to seeing Chrome vs. Firefox found it an
easy conceptual extension. But Go hasn't been marketed as a systems language
for a long time and Rust removed all its Go-like features years ago; the "Go
vs. Rust" that we still see is just residual fallout.

~~~
notheguyouthink
It's funny, as a Go _and_ Rust fan, they're still in the same space to me
conceptually.

I'm sticking with Go at the moment because moving my shop to Rust is quite
difficult _(for varying reasons)_ , and because Go offers a nice compromise on
ease and power. However i miss no-null types _so bad_. All that compile-time
information is amazing.

I really hope Go adopts some features from Rust.

------
lobster_johnson
This looks very nice. I've been working with sum types (as far as they can be
called that) in Go a lot recently, and it's frustrating how bad it is as
algebraic data types in general.

Aside from the lack of exhaustiveness checks, one particularly annoying area
is writing code for enumeration and transformation. I needed one set of types
to have this:

    
    
        type Node {
          Walk(WalkFunc)
          TransformBottomUp(TransformFunc) (Node, error)
          TransformTopDown(TransformFunc) (Node, error)
        }
    

I implemented it as an interface precisely because of the lack of
exhaustiveness checks, even though it feels wrong to attach code to my pure-
data types.

But the code to do it is pure boilerplate. I considered doing something like
having GetChildren() and SetChildren() and writing some generic transformer
code, but they you have to deal with slices, which means a lot of allocation.
I also considered having interfaces like Nonary, Unary, Binary, etc. that
encapsulated the number of children, but that's terrible, too.

I might go with simple switch statements instead, and use this utility.

------
cube2222
I think that switching on types in Go is gererally a total anti-pattern.

In case you need to handle errors, in the end the type doesn't matter at all
for you. You are interested what the implications of the error are, so the
library exposing the error should just expose functions like isTemporal(e
error) bool. In case you need to inform the user about the exact error, no
problem, the returned error has the Error() function which shows the message,
no matter the type, use that.

In case you're not matching errors, then you shouldn't worry about the type.
The function returns an interface because all that should matter to you is
that interface with its common behavior.

Otherwise you're just fighting the language, which doesn't make any sense, as
you can just switch to Rust then.

EDIT: Though I'm really happy that people make more tools for analyzing Go
code. Good job!

~~~
burntsushi
> I think that switching on types in Go is gererally a total anti-pattern.

It's not. It's a standard way of implementing variant types. See
[https://golang.org/doc/faq#variant_types](https://golang.org/doc/faq#variant_types)
and
[https://golang.org/src/go/ast/walk.go?s=1311:1342#L41](https://golang.org/src/go/ast/walk.go?s=1311:1342#L41)

True, it's not used often. Sum types aren't a great fit for Go (as the FAQ
says), but sometimes they are exactly what you want.

> Otherwise you're just fighting the language, which doesn't make any sense,
> as you can just switch to Rust then.

I find these types of comments really dismissive and unhelpful. A single
comment declaration and one Sunday's worth of hacking (thanks to the
incredible tooling provided by Go) makes my experience using Go better. Why do
I need to switch to Rust? It would take several months and significant social
capital to do. (And I'm saying this as a member of the Rust library team!)

~~~
cube2222
Actually, after reading the walk.go code I still wonder what the best way to
do it would be. (Not saying it's bad now, though I'll still remain my devils
advocate)

As I skimmed through the code, in my opinion you could easily have a Walk(v
Visitor) method on each of those node types.

This way you'd have no need for casting, and in the Walk method you would be
able to use type-specific functionality without the need to cast. The only
disadvantage I see is that the Walking functionality would be spread out among
the code in different places, but this isn't a problem with modern editors.

I'll note this down somewhere and if I have time I'll test it out sometime.

~~~
pbnjay
Great minds think alike (IMHO)

[https://www.reddit.com/r/golang/comments/60dik1/a_utility_fo...](https://www.reddit.com/r/golang/comments/60dik1/a_utility_for_running_exhaustiveness_checks_on/df669jk/)

~~~
cube2222
Anyways, my conclusion is. In most cases it IS an anti-pattern. Only if the,
let's say "observing" function (the one that decides on the type), is really
just observing the behavior nad trying not to interwind with the mechanics of
the actual structures is it justified I think. In that case it provides
greater cleanliness of your code.

In this case though, as this is the ast package, and walking is a standard
functionality of an ast, I think this could just as well be implemented with
interfaces.

------
gothrowaway
Golang irritates people for various reasons. For some reason I've begun to
feel the language is was partly designed to put prima donnas who push
theoretical languages in blogs all day rather than ship in their place.

These Haskell and Scala people talk talk talk all about correctness and syntax
tricks. Meanwhile golang is nimble and just winning every race. I wish they'd
learn from it rather than get so defensive.

Also, it's just a confirmation people get too attached to complicated features
in languages as a crutch. YAGNI

Programming _can_ be straight-forward and clear. Without it spiraling into a
contest of clever tricks. You look clever when you get product out the door.

I've been programming 10 years professionally, and 15 years as a hobby. I've
yet to hear what a sum type is until yesterday.

I got nothing wrong with people dabbling and hacking on the weekend, but
sometimes it irks me when people split hairs over being "correct" and never
actually get stuff done.

I think a lot of the "syntax tricks" is a mechanism used by people to feel
superior. It's like, "hey if I can't actually build something useful, at least
I can blog about how to write a monad transformer".

I'm not trying to be offensive, I'm just failing to see the value in this and
trying to make sense of what I'm seeing. Where was the business issue that
could _only_ be solved through sum types in the language syntax? Enlighten me.

~~~
burntsushi
In our code, we have a closed interface type with many variants, not unlike an
AST. We frequently add new variants to that interface. It is nice to have a
tool tell us which parts of the code need updating to account for the new
variant. This tool does that. It trivially reduces bugs and saves development
time.

If you think people like me "don't get stuff done," then please, take a stroll
through my Github. ;-)

