Discriminated Unions and Enums Example

210 views
Skip to first unread message

Cliff

unread,
Sep 1, 2025, 1:50:49 PM (4 days ago) Sep 1
to golang-nuts
Hi Gophers!

I'm trying to find the right place for this work. I spent a couple of weeks looking at early proposals and dev-team reactions to Discriminated Unions and Enums as well as feedback from surveys and open/closed proposals. 

I developed box as a fundamental/orthogonal type in the compiler based on that research and I thought maybe it would serve as a discussion point - since it's an implementation that can actually be experimented with. 

https://github.com/CliffsBits/discriminated_unions_and_enums_for_go 

I don't think it will break any existing codebases. 

Jason E. Aten

unread,
Sep 1, 2025, 6:47:50 PM (4 days ago) Sep 1
to golang-nuts
I'm neutral. 

+ I like the exhaustiveness checking this enables.

+ I like the potential for efficiency if only one of the union types needs to
be allocated and there are a large number of possibilities.

- I don't like the re-use of the type switch because it hurts the readability in
that currently a type switch means an interface is being operated on. With the
proposed syntax, it becomes ambiguous: it could be a box or an interface.

- I don't like the implicit conversion of a type into a box that the divideBox()
example does, as there could be many types that could convert and it
is not clear how the reader would or could know which conversion happened.
It might be clearer to have names for the fields instead of just types, like a struct. Then they
could be assigned to directly. This would also help with serialization 
and deserialization of a box to disk or network.

- I don't like there being a new way to return an error, in effect hiding the
fact that an error was returned inside a box. Having multiple return
values with one of them being an error has become idiomatic, and
I find this helps increase the understandability of code dramatically.

open questions:

_ what happens when interfaces are choices inside a box? Go values
orthogonality and composability, so this would be a natural thing 
for a developer to do.

_ is there some way to use boxes to support chaining when referring
deep into a tree of boxes?; so if I would normally say a.b.c.d.someMethod() but
it turns out c is a box with a nil pointer (option type), then are the
boxes type-checked for compatibility with chaining, and can the
error propagate up...


Stephen Illingworth

unread,
Sep 2, 2025, 4:04:15 AM (3 days ago) Sep 2
to golang-nuts
On Monday, 1 September 2025 at 23:47:50 UTC+1 Jason E. Aten wrote:
- I don't like there being a new way to return an error, in effect hiding the
fact that an error was returned inside a box. Having multiple return
values with one of them being an error has become idiomatic, and
I find this helps increase the understandability of code dramatically.

I agree with this but I have a slightly different view:

Because the error type cannot be inside a box (because it's an interface and
not a concrete type) the idiom will still require the programmer to return multiple
values if an error is involved. I don't like the suggestion in the experiment that it
provides alternative error handling possibilities. If we could box error types
then maybe, but not otherwise.


But I have to say Cliff, I really appreciate the effort you've put into this. The documentation
you've written is clear and you've obviously spent a lot of time on it. I look forward
to seeing how other people react and whether the experiment can turn into a proposal.

Axel Wagner

unread,
Sep 2, 2025, 5:01:17 AM (3 days ago) Sep 2
to Jason E. Aten, golang-nuts
Hi,

On Tue, 2 Sept 2025 at 00:48, Jason E. Aten <j.e....@gmail.com> wrote:
I'm neutral. 

+ I like the exhaustiveness checking this enables.
[…]

_ what happens when interfaces are choices inside a box? Go values
orthogonality and composability, so this would be a natural thing 
for a developer to do.

I believe these two are incompatible under this approach - or at least, if you want both, you need to employ a SAT solver (see below).
 
- I don't like the implicit conversion of a type into a box that the divideBox()
example does, as there could be many types that could convert and it
is not clear how the reader would or could know which conversion happened.
It might be clearer to have names for the fields instead of just types, like a struct. Then they
could be assigned to directly. This would also help with serialization 
and deserialization of a box to disk or network.

Yes, this is the core design decision to make `box` a union type, instead of a sum type. It is also this decision that means you can't have both interfaces as cases and exhaustiveness checks. Basically, because in a union type the cases can overlap (if we allow interfaces), a switch statement becomes a boolean formula in Disjunctive Normal Form (every interface case is a boolean variable of "has method X and Y and Z". You can model negation by including conflicting method signatures). Proving that all cases are covered requires proving that this formula is a tautology, which requires proving that it's negation is not satisfiable. The negation of a DNF formula is in CNF and proving whether or not a CNF formula is satisfiable is NP-complete.

Sum types solve this, as there is always a finite, non-overlapping list of cases, so proving that a switch/match covers all cases basically just means ORing and ANDing a bunch of bit masks and check that the OR has all 1s and the AND has all 0s. Sum types are significantly more powerful and if we introduced a new syntax, I would hope that we would use sum types. I think the only reason we should use union types is because we want to reuse the `interface{ a | b | c }` syntax (which has other problems as well, like requiring a `nil` zero value).

I will say, that I believe it is these kinds of design questions that are holding up any sort of variants in Go. It seems most likely (based on statements by the Go team) that we would not want to add a new concept that has this much semantic overlap with `interface{ a | b | c }`. But if we did that, the result would lack most of the power people want from it. A lot of people have been clear, that in their opinion it would not be worth bothering at all in that case.

If we could agree what we want, I don't think the implementation would really be the hurdle. But the effort is impressive.


- I don't like there being a new way to return an error, in effect hiding the
fact that an error was returned inside a box. Having multiple return
values with one of them being an error has become idiomatic, and
I find this helps increase the understandability of code dramatically.

open questions:



_ is there some way to use boxes to support chaining when referring
deep into a tree of boxes?; so if I would normally say a.b.c.d.someMethod() but
it turns out c is a box with a nil pointer (option type), then are the
boxes type-checked for compatibility with chaining, and can the
error propagate up...


On Monday, September 1, 2025 at 6:50:49 PM UTC+1 Cliff wrote:
Hi Gophers!

I'm trying to find the right place for this work. I spent a couple of weeks looking at early proposals and dev-team reactions to Discriminated Unions and Enums as well as feedback from surveys and open/closed proposals. 

I developed box as a fundamental/orthogonal type in the compiler based on that research and I thought maybe it would serve as a discussion point - since it's an implementation that can actually be experimented with. 

https://github.com/CliffsBits/discriminated_unions_and_enums_for_go 

I don't think it will break any existing codebases. 

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/5b523201-28ed-4332-a5f9-aa9fba6ed65bn%40googlegroups.com.

Axel Wagner

unread,
Sep 2, 2025, 5:04:36 AM (3 days ago) Sep 2
to golang-nuts
I should proof-read *before* hitting send:

On Tue, 2 Sept 2025 at 11:00, Axel Wagner <axel.wa...@googlemail.com> wrote:
It seems most likely (based on statements by the Go team) that we would not want to add a new concept that has this much semantic overlap with `interface{ a | b | c }`. But if we did that, the result would lack most of the power people want from it.

To clarify: by "if we did that" I mean "reuse `interface{ a | b | c }`", not "add a new concept".

Cliff

unread,
Sep 2, 2025, 6:56:19 AM (3 days ago) Sep 2
to golang-nuts
Thanks for engaging. It was a lot of work. Per observations from 2011, interfaces in discriminated unions creates a mess. This implementation avoids that mess by disallowing them. Interfaces are extremely good at providing polymorphism for behaviors. Sum types/Discriminated unions serve a different purpose (closed set semantics).

Errors - not being able to return a generic error was a disappointing consequence of the design - but you can return concretely typed error structs. It's a different pattern, it's more work, but it offers the safety guarantees that some vocal subset of people who exist have been talking about. 

To be clear on intent - I wanted to see what it would take to build the concept in the go compiler. The limitations of the design are what I found to make it work well with the rest of the language design. Of the language design tools we have (especially in a procedural context), sum types are the weakest. The go team chose structs and interfaces from the beginning and I agree with their assessment that those cover 80% of the use-cases you'd have in a language.  The enum and discriminated union / sum type workarounds that the community has found work. The only point I'd make to that - is if so many people have built packages and linters - maybe it's a missed feature opportunity.

There may be syntactical choices to push a concept like box further from other constructs in the language visually, but the design choice here was to keep it familiar. This answers "is there a set of constraints under which sum types / discriminated unions / enums can easily coexist in go" and the answer is yes. 

I think whatever concept Rob and other designers had of a sum type in 2011 is quite different from what we've seen in Rust, Typescript, Gleam etc. 

From an implementation, language theory, and sanity point of view: putting non-concrete types in a sum type won't work. If the design principles of go demand interfaces in sum types, they will never be workable. The one 'escape hatch' for this I could see is using bounded interfaces where you limit the concrete types that can have that interface and then use those.


type example interface (typea | typeb)  {
    area() float64
    perim() float64
}

You could use an interface like that in a sum type because it is explicitly bounded, but I think it's an ugly concept that is only necessary if you want to wedge an open-set behavioral contract concept into a closed set identity concept.  A concept and syntax similar to this explanation (in support of generics) https://go.dev/blog/intro-generics would be necessary.

it is my opinion that sum types are low-hanging fruit that demand no change if implemented without interfaces. They remain as complicated and disruptive as the team has maintained if you do. There is huge value in a small language and using extension budget for a feature like this this is a social/value judgement that has technical impacts. 

tldr; Sum types are trivial if you don't allow behavioral contracts or anonymous types in. Sum types are doable if you allow type restrictions on interfaces. Sum types are not doable without a) compromise or b) change.

Thanks for looking y'all - seriously - this was a design exercise for me - and it helped me understand the go-team's perspective. 

Axel Wagner

unread,
Sep 2, 2025, 7:13:28 AM (3 days ago) Sep 2
to Cliff, golang-nuts
On Tue, 2 Sept 2025 at 12:56, Cliff <cliff.prog...@gmail.com> wrote:
From an implementation, language theory, and sanity point of view: putting non-concrete types in a sum type won't work. If the design principles of go demand interfaces in sum types, they will never be workable.

Why not? In a union they don't work, but in a sum I can't see a reason why they wouldn't work. If I'm overlooking something, I'd be super interested (I intend to give a talk on this topic soon, so I would want to avoid being wrong).

Have you seen https://github.com/golang/go/issues/54685 ? It has a very well-worked design for how sum types could work in Go. I have not found a problem with it and it doesn't preclude using interfaces.
 
The one 'escape hatch' for this I could see is using bounded interfaces where you limit the concrete types that can have that interface and then use those.


type example interface (typea | typeb)  {
    area() float64
    perim() float64
}

You could use an interface like that in a sum type because it is explicitly bounded, but I think it's an ugly concept that is only necessary if you want to wedge an open-set behavioral contract concept into a closed set identity concept.  A concept and syntax similar to this explanation (in support of generics) https://go.dev/blog/intro-generics would be necessary.

it is my opinion that sum types are low-hanging fruit that demand no change if implemented without interfaces. They remain as complicated and disruptive as the team has maintained if you do. There is huge value in a small language and using extension budget for a feature like this this is a social/value judgement that has technical impacts. 

tldr; Sum types are trivial if you don't allow behavioral contracts or anonymous types in. Sum types are doable if you allow type restrictions on interfaces. Sum types are not doable without a) compromise or b) change.

Thanks for looking y'all - seriously - this was a design exercise for me - and it helped me understand the go-team's perspective. 

On Tuesday, September 2, 2025 at 5:04:36 AM UTC-4 Axel Wagner wrote:
I should proof-read *before* hitting send:

On Tue, 2 Sept 2025 at 11:00, Axel Wagner <axel.wa...@googlemail.com> wrote:
It seems most likely (based on statements by the Go team) that we would not want to add a new concept that has this much semantic overlap with `interface{ a | b | c }`. But if we did that, the result would lack most of the power people want from it.

To clarify: by "if we did that" I mean "reuse `interface{ a | b | c }`", not "add a new concept".

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Axel Wagner

unread,
Sep 2, 2025, 7:20:01 AM (3 days ago) Sep 2
to Cliff, golang-nuts
FWIW you can model a sum as a product plus a tag. That is, you can implement

Maybe[T] = None | Some T
match m {
case None:
    // do thing
case Some v:
   // do thing with v
}

as

type Maybe[T any] struct {
    Case int // 0 or 1
    None struct{}
    Some T
}
switch m.Case {
case 0:
    // do thing
case 1:
    v := m.Some
    // do thing with v
}

This seems fairly straight forward, and it would work fine with interfaces. It's not an incredibly efficient implementation. But (especially if we allow reordering of struct fields, which I believe is something the Go team is considering) the compiler can always pack things more efficiently.

Sandesh Gade

unread,
Sep 3, 2025, 1:29:33 AM (2 days ago) Sep 3
to golang-nuts
First of all, thank you for your effort into this! Impressive stuff. I am not a compiler expert, but the repo made it easier to understand the ideas you are talking about for someone like me. That being said, my following questions are a step towards understanding what is and isn't possible in this design? 

Question 1, you've made it clear that interfaces can't be types inside a box type, however, is the opposite true? Can a box type satisfy an interface?

```go
type Red struct{}
func (r Red) String() string { return "red"}

type Green struct{}
func (g Green) String() string { return "green" }

type Blue struct{}
func (b Blue) String() string { return "blue"}

type Color box {
    Red
    Green
    Blue
}

...
var c Color
...
```

Does `c` satisfy fmt.Stringer?

Question 2, is there scope to define methods on a `Box` type? Would defining `func (c Color) String() string { ... }` satisfy `fmt.Stringer`?

Cliff

unread,
Sep 3, 2025, 7:56:45 AM (2 days ago) Sep 3
to golang-nuts
I got hit by a car last night. As I was sitting in the emergency room I realized the answer to Russ Cox's 2011 riddle https://groups.google.com/g/golang-nuts/c/0bcyZaL3T8E - you disambiguate as you're placing the item in the sum-type/box. 

type RW union {
io.Reader
io.Writer
}

mean? Or is it disallowed?
(That would be pretty unfortunate.)

What happens if you have

var r io.Reader = os.Stdin  // an *os.File
var rw RW = r <- io.Reader //this disambiguation is actually not necessary - if you placed it as an io.Reader it should come out as an io.Reader - but if the os.Stdin had been placed 'raw' it would have needed a disambiguation tag. I'm not suggesting syntax (as, inShapeOf... a bunch of syntax is possible). Basically - it's manually setting the tag on the discriminated union so there is no ambiguity on retrieval. 

I haven't written disambiguation syntax - I'd be happy to revisit the project if someone wants a 'complete' tool to play with - but with that as an option - I consider simple sum-types solved. The compiler source-code already has a really solid example that was introduced for generics. I think the easiest case forward would be to no longer call {type1|type2} an interface for generics - find a new word - and add disambiguation logic on insert for the classic Russ Cox case, or use 'box' as a keyword because it's neat.

(Apologies - I don't know how to quote properly) 

Regarding https://github.com/golang/go/issues/54685 - sigma types. They're more complex and powerful - they could work if that's what you guys want. I prefer simple (I think the box concept is simple).

Question 1, you've made it clear that interfaces can't be types inside a box type, however, is the opposite true? Can a box type satisfy an interface?
 
Answer: as implemented - boxes only expose the methods to get a concrete type out of a box - they are not 'the thing itself', they're just a wrapper for a variable. 

Question 2, is there scope to define methods on a `Box` type? Would defining `func (c Color) String() string { ... }` satisfy `fmt.Stringer`?
Answer: Same as above - boxes are really dumb containers. 
Reply all
Reply to author
Forward
0 new messages