https://github.com/golang/go/issues/76920
This is an issue to discuss yet another form of sum type to Go. This is not a full proposal, and not a language change that I expect will be adopted in the near future. I am filing this issue as a point of reference to discuss this particular approach.
A sum type, tagged union, variant, variant record, choice type, discriminated union, disjoint union, or coproduct is a type which can hold a value of one of a fixed set of types. (Thanks Wikipedia for the list.) Rust calls these enums, which means that when someone says Go should support "enums" they might be asking for a C-style enumeration of integers or a Rust-style enumeration of types.
I posit that most people who want this feature in Go are looking for something along the lines of a Rust enum: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html
To guide what follows, I'm going to also propose a few motivating examples which any design should support well.
[4]byte IPv4 address or a [16]byte IPv6 address.Many proposals for sum types in Go aim to extend interface types to cover this case. Since interfaces already support listing a set of type variants for use in type constraints, it's tempting to say that we could just permit something like this:
type IPv4 [4]byte
type IPv6 [16]byte
type IP interface { IPv4 | IPv6 }
I think that while this is tempting, it's the wrong approach.
First, this doesn't support the Direction case well at all. Do we need to define types for North/South/East/West?
The zero value of an interface is nil. Do we require that sum types all have an additional zero-value case that users need to handle, even if they don't actually want a zero value?
You need to define top-level symbols (types) for every variant. That's not so bad in the IPv4/IPv6 case, but it's not difficult to imagine types where this is a problem.
The syntax doesn't scale to lots of variants.
I just don't like this approach; I think it's trying to force two similar but different concepts (interfaces and sum types) together in a way that doesn't work well.
Go structs are a "product type" in the terminology of type theory. Let's define a sum type using similar syntax to our product types. We'll call a sum type a "union" in the following, but I'm not wedded to the name:
type Direction union {
North, South, East, West atom
}
type Maybe[T any] union {
Unset atom
Set T
}
type IP union {
IPv4 [4]byte
IPv6 [16]byte
}
This is exactly the struct syntax, except instead of fields we'll have "variants".
I've also introduced a new predeclared type here, called "atom". (Again, I'm not wedded to the name. "unit" might also work.) An atom is a struct{}, just like an any is an interface{}:
type atom = struct{}
We use atoms to indicate variants that have no data. The new identifier is introduced because it looks weird and hacky to write struct{} all over the place and struct{}{} is worse.
A value of a union type always contains exactly one of the type variants. The zero value of a union is the zero value of the first variant, so for our above examples: North, Unset, and 0.0.0.0. Yes, this means the order of the variants in the declaration is important. So is the order of values if you use iota to define an enum with something like const ( North = Direction(iota); South; East; West ). It's fine.
Variants of a union may be named with a selector expression on the type like T.Variant. This is going to be ambiguous with method selectors, but selectors are already ambiguous on fields and methods so I think that's fine.
For a variant of type atom, the selector expression evaluates to a value of the union type with that variant. So for example:
d := Direction.North // d is a Direction with variant North
if d == Direction.North {
}
switch d {
case Direction.North:
case Direction.South:
case Direction.East:
case Direction.West:
}
For a non-atom variant, evaluating the selector expression is an error.
m = Maybe[int].Set // error: evaluating non-atom union selector
As with structs, a selector expression may be used on a value of a union type.
In an assignment statement, a selector expression sets the value of a union to that variant.
// Set m to the "Set" variant with value 42.
var m Maybe[int]
m.Set = 42
m.Unset = atom{} // but m = Maybe.Unset is probably clearer
// Set ip to 127.0.0.1
var ip IP
IP.IPv4 = [4]byte{127, 0, 0, 1}
When evaluated, a selector expression on a non-atom variant yields two values: The value of the variant and an untyped bool indicating whether that is the currently set variant.
A selector expression on an atom variant yields only one value: An untyped bool indicating whether that is the currently set variant.
var m Maybe[int]
if v, ok := m.Set; ok {
// v is the value of the Maybe
}
if m.Unset {
// the Maybe is not set
}
To avoid accidents, it is an error to assign a atom variant to two values, or a non-atom variant to a single value.
v := m.Set // error: this must be v, ok := m.Set
v, ok := m.Unset // error: this must be just v := m.Unset
Composite literals of union type must contain at most one keyed element. They may not contain elements without a key.
d := Direction{North: atom{}} // same as d := Direction.North
m := Maybe[int]{Set: 42}
m := Maybe[int]{42} // error: elements must be keyed
ip := IP{IPv4: [4]byte{...}, IPv6: [16]byte{...}} // error: too many elements
We add a new form of switch statement, the "union switch". This behaves much the same as a type switch, but over the variants of a union.
switch d.(union) {
case North:
case South:
case East:
case West:
}
var m Maybe[int]
switch v := m.(union) {
case Unset:
// v is not in scope here.
case Set:
// v has type int
}
Union switches must be exhaustive. You can include a default case if you want a non-exhaustive switch.
// Error: Failed to handle all cases.
switch d.(union) {
case North:
}
// This is fine.
switch d.(union) {
case North:
default:
}
Union variants are not addressable. (But you can put a pointer to something addressable in a variant if you want.)
Unions are comparable if all their variants are comparable.
Q: Do we need this atom type? We could just omit the type from the union definition for the "has no value case".
That works, but it looks a lot like an embedded type:
type Maybe[T any] union {
Unset // looks like an embedded Unset type
Set T
}
Also, atoms are pretty nice for non-union uses, like map[string]atom or chan atom.
Q: How about just Direction{North}? Direction{North: atom{}} is weird.
This works, but Direction{North} looks like an unkeyed composite literal and we're going to interpret North as a key. Maybe that's okay. But you can just write Direction.North, which is simpler.
Q: How about m := Maybe[int].Set(42), as opposed to Maybe[int]{Set: 42}?
I considered extending type conversion syntax for creating a union variant in this fashion. I don't like it, though, because it makes the variant look like a type. One of my notions in this design is that while in programming language theory each variant may be a type, expressing those types in the Go type system just adds confusion.
Q: What if I write x := someMaybe.Set?
Evaluating a selector for a non-atom variant always yields two values, and it is an error to assign the selector to a single value. This isn't like map access where the second ", ok" value is optional.
If we allowed a selector like this to evaluate to a single value, we'd have two choices:
It could evaluate to the value of the variant, and panic if that isn't the current variant. People hate panics. If you're really certain of the currently selected variant, you can write x, _ := someMaybe.Set and discard the bool.
Alternatively, it could evaluate to a bool indicating whether the variant is selected, so you could write if someMaybe.Set { … }. However, this invites incredibly confusing errors if the value of the variant is a bool:
m := Maybe[bool]{Set: false}
if m.Set {
// This executes, because Set is the selected variant,
// even though the *value* of set if false...
}
So, no, selectors on non-atom variants always yield two values.
Q: I really don't want adding a new variant to my exported union to be a breaking API change, but the exhaustive type switch requirement makes it one.
Add a blank case to your union:
type Extendable union {
_ atom // default case
North, South, East, West
// new directions to be added as science discovers them
}
Now type switches all need to contain a "default:" clause. Also, your zero value is now distinct from any of the defined variants.
Q: Can I have methods on a union type?
Of course, it's just a type.
type IP union {
IPv4 [4]byte
IPv6 [16]byte
}
func (ip IP) String() string { ... }
Q: Do you think we should add this (or some form of sum type)?
I don't know.
The argument in favor is that this is one of the most common language feature requests (usually phrased as a request for "enums", which may indicate that this often comes from frustrated Rust programmers). Some form of union or sum type is a conventional language feature, and it's perhaps a bit strange that Go doesn't have one. I think the above design also fits fairly nicely into the language; I don't think we'd have regretted it if it had been present in Go 1.0.
Related Issues
Related Discussions
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
I believe unions as sigma types is very similar to this proposal. Same idea about using struct{} as a placeholder values.
It's likely that I misunderstand, but when I read this there seems to be an ambiguity at the heart. It may not be an important ambiguity, but it puzzles me.
type atom = struct{}
atom is a type alias for struct{}.
A value of a union type always contains exactly one of the type variants.
So a union type is a list of types, and a value of a union type holds a value of exactly one of those types.
Go type IP union { IPv4 [4]byte IPv6 [16]byte }
Here IP is a list of types: [4]byte and [16]byte. Within the content of IP, we names those alternatives IPv4 and IPv6. The name IPv4 and IPv6 are meaningless except in the context of IP. They are not types, they are selectors akin to struct fields. If x is a variable of type IP, then x.IPv4 is a value of type [4]byte.
Go type Direction union { North, South, East, West atom }
Here Direction is a list of types? But there is only type listed here: atom, which we've already established is an alias for struct{}. But there are four names listed for that type. So does that mean that North, South, East, and West are types? But in the IP example it seems clear that IPv4 and IPv6 are not types; they are values. If dir is a variable of type Direction, what is the type of dir.North? The proposal implies that the type of dir.North is Direction.North, but that is quite different from x.IPv4.
What happens if I write
type AxisValue union {
X, Y int
}
Does this mean that I can store distinct int values in AxisValue by referring to the X or Y field?
Or is it the case that atom is a sort of reverse type declaration, in that an identifier that appears to have type atom instead becomes itself a named type of type struct{}?
Nice work. I like it, although I don't have the expertise to know if it has footguns or not. I will leave that critique to those who are more skilled at language design.
One bikeshed though: While I understand the reason for atom I find the word itself requires me to mentally translate it to a concept that I can reason about, much like someone has to do mental translation for a human language they are not fluent in.
What is name of the concept that doesn't require me to do mental translations? null. That is what I would propose you name the thing which your proposal calls an atom, assuming this proposal goes anywhere, that is. #jmtcw #fwiw
So a union type is a list of types, and a value of a union type holds a value of exactly one of those types.
No, a union type is a list of variants. Each variant has a name and a type. A value of union type contains exactly one of those variants at any time.
In the Direction case, there are four variants each of which have the same type. The type is struct{} (atom), because we only care which variant is selected.
type AxisValue union {
X, Y int
}
In this case, there are two variants, X and Y, each of which have type int. You can have an AxisValue{X: 10} or AxisValue{Y: 10}, but not AxisValue{X: 10, Y: 10} (only one variant). The zero value AxisValue{} is the same as AxisValue{X: 0} (zero value of the first variant).
The way I'm understanding this is that the representation of a union type includes an opaque identifier for which variant is selected along with the data for that variant. In the case where the variant has type struct{}/atom the data part is zero-length but the variant is still stored, so in the Direction case (where all the variants have type atom) the representation includes only the variant identifier and no data portion at all (if you prefer: a zero-length data portion), but the variant is still part of the value rather than being a type in its own right.
Presumably for this to hold it must be impossible to take the address of a specific variant, like &addr.IPv4, because that would be invalidated if addr gets overwritten with a value of a different variant. But I think that is consistent with the rest of the proposal because it establishes that the expression addr.IPv4 always produces two results (the second being ok bool) and so it wouldn't be valid to take the address of that expression anyway.
It's unfortunate that &addr.IPv4 would be valid if addr were of a struct type with an IPv4 field, but this doesn't seem too different to how it's valid to take an address of a slice element but not of a map element.
A minor quibble about this example, though mainly just to confirm I'm understanding the proposal correctly:
switch d.(union) {
case North:
case South:
case East:
case West:
}
var m Maybe[int]
switch v := m.(union) {
case Unset:
// v is not in scope here.
case Set:
// v has type int
}
I'm understanding a union switch as just being a switch on the opaque "variant id" portion of the value, using the terminology from my previous comment.
Therefore I would expect that in the case Unset branch v there would be v symbol defined, but its type would be struct{}/atom, right? Or is there something more special going on here that I'm not appreciating?
(Sorry for so many comments. I'm trying to keep my separate reactions separate.)
If a union switch is required to be exhaustive then that implies it's a breaking change to add a new variant to a union type in a later release, because it would cause any existing switch over that union type to fail to compile.
Do you think this proposal ought to have something similar to Rust's #[non_exhaustive] attribute, which would make it mandatory to include a default: clause in any union switch for a particular union?
A strawman syntax for that:
type ProgressToken union {
S string
I int
// the following ellipsis means "and possibly other new variants in later releases".
...
}
Though I do worry that the ... token is a little too subtle for an unfamiliar reader to immediately guess what it represents, and that perhaps this should be inverted so that non-exhaustive is the default and "I promise there will never be any other elements" is what has additional syntax, since that's the most constraining of the two possibilities. 🤔
Thanks.You say that a union type is a list of variants, and each variant is a name and a type. To be clear, I think you are also saying that a value of a union type contains exactly one variant plus a value of that variant's type.
If a union is a list of variants, then why is Direction.North a value but IP.IPv4 is not a value? I guess that you are saying that for the special case of a variant of type struct{}, then you are permitted to write Direction.North. It means something like a value of type Direction that contains the variant of name North and type struct{} and value struct{}{}.
But for a variant whose type is not struct{} you are not permitted to write IP.IPv4.
To put it another way, if a union is a list of variants, and each variant is a name and a type, then the type Direction has four variants: North, South, East, and West. Each of those variants has type struct{}. And I'm permitted to write Direction.North but that is only permitted for a variant of type struct{}. And what it means is a value of type Direction, with variant North, and value struct{}.
A union switch also apparently has a special case for variants of type struct{}, in that for v := u.(union) the identifier v is not in scope if the case is the name of a variant of type struct{}. I don't see why. It would seem more consistent for the identifier to be in scope with a value of type struct{}{}.
Selector expressions also have a special case for variants of type struct{}. Instead of getting the value struct{}{} you just get a bool.
Maybe the differing treatment of the type struct{} is a good idea but it seems a bit confusing. And at least for me calling it atom doesn't make it less confusing.
I think that I personally would find it less confusing if we didn't try to handle variants differently based on the type, but instead made the names different. A union is a list of variants. Each variant has a name. Some variants also have types. When defining a union type you can write a variant either as name type or as name UNTYPED, where UNTYPED is an indicator that the name has no type. In your examples UNTYPED is spelled atom. That works but I'm not crazy about it. I'm not tied to this, but my first suggestion would be that we spell it iota. That reuses the existing predeclared identifier in a somewhat similar context: each use of iota in a union marks a distinct name.
Whatever we call it, we are clear that a value of a union type whose variant has no type consistently has no type, from which follow the rules about referring to that variant of a union, and about union switches.
If a union is a list of variants, then why is Direction.North a value but IP.IPv4 is not a value?
The short answer is: Practicality.
IP.IPv4 is not a value because there no meaningful value for it to be. (We could say that IP.IPv4 is a type, but that would be a different proposal.) Direction.North is a value because we want a concise way to write "the direction north".
If we don't special-case variants with no associated data, then they are cumbersome to use. For example, let's say we treated all variants the same:
// Composite literal: We need to provide a meaningless value for the key North.
d := Direction{North: struct{}{}}
// Assignment: Again, we need to provide a meaningless value.
d.South = struct{}{}
// Access: We need to discard a meaningless value when checking the direction.
if _, ok := d.East; ok {
}
I believe that the no-data variant cases are important and common, and we should ensure that they are ergonomic to use.
When defining a union type you can write a variant either as name type or as name UNTYPED, where UNTYPED is an indicator that the name has no type.
This is an option, but it seems more complex than saying that variants always have a type. We would still have the special cases for accessing dataless variants (someUnion.Field behaves differently depending on the type or lack thereof of Field), but we'd also have an additional special case when defining fields.
Another possibility that I considered (but don't really like) is saying that a variant may have a type or a value:
type Maybe[T] union {
Unset true
Set T
}
m := Maybe[int]{Unset: true}
m.Unset = true
// Errors: Unset may only be true
m = Maybe[int]{Unset: false}
m.Unset = false
it must be impossible to take the address of a specific variant, like &addr.IPv4, because that would be invalidated if addr gets overwritten with a value of a different variant
Yes, variants are not addressable, for this reason and also to permit an efficient memory representation. (For example, the storage for the IPv4 and IPv6 variants might overlap in memory.)
Therefore I would expect that in the case Unset branch v there would be v symbol defined, but its type would be struct{}/atom, right? Or is there something more special going on here that I'm not appreciating?
You're right; it's simpler in this case if v was defined in this branch with type struct{}. Adding a special case for atoms in this case doesn't provide any benefit.
Do you think this proposal ought to have something similar to Rust's #[non_exhaustive] attribute, which would make it mandatory to include a default: clause in any union switch for a particular union?
I've got an entry in the FAQ about this. :)
Add a blank variant to the union to make it non-exhaustive:
type Extendable union {
_ atom // default case
North, South, East, West atom
// new directions to be added as science discovers them
}
There is no way to reference the _ variant, so switches will always need to include a default: branch. (This also has the effect of making the zero value of the union something distinct from the named variants.)
Ah, apologies for missing that FAQ entry about nonexhaustive matching. I realise now that I did read it at first but then I distracted myself with my other questions and forgot that I'd read it. 🤦🏻♂️
I agree that an unnamable variant is a reasonable way to represent that, although I do find it (subjectively) a little quirky.
I still wonder if it wouldn't be better to require a default case by default and let a specific type opt out of that by promising that no variants will be added later -- that's the safer default, after all -- but I don't really know what syntax I'd propose for that because there isn't anything else in Go that works like that. And indeed, maybe that's a good enough reason against it: it would be unlike anything else in Go.
Treating struct{} differently is going to be confusing for people who want to write
type Ordering union {
Big endian.BigEndian
Little endian.LittleEndian
}
because as it happens endian.BigEndian and endian.LittleEndian are names for struct{}.
Unless you say that only plain struct{} (or aliases of plain struct{}) is treated specially, and then it is even more of a special case.
Also if struct{} is special then it becomes a breaking change to add a field to an exported empty struct type.
@neild —
For the case of unions like Direction:
type Direction union {
North, South, East, West atom
}
How would it handle fmt.Stringer? Would we be required to implement a boilerplate method such as below:
func (d Direction) String() string {
switch d.(union) {
case North:
return "North"
case South:
return "South"
case East:
return "East"
case West:
return "West"
default:
return "Unknown"
}
}
Or could you envision these unions automatically get a default String()string implementation if one is not explicitly provided to avoid having to implement and maintain such boilerplate?
Also, you mention special-case variants with no associated data, but there is metadata such as a custom fmt.Stringervalues that could be associated, if designed to support that, of course. I know we could possibly use tags for that like we do JSON property names, but would it not be preferable to get type safety for future language features when we know in advance what we need to implement?
I think a String method with the obvious defaults for variants whose type is the empty struct, or whatever, would be a natural thing to add to golang.org/x/tools/cmd/stringer. I don't see a reason why the language should have builtin support for it. Especially since not everybody would want it.
That said, it's worth asking how these values should be handled by fmt.Print. Maybe there is a natural choice that would obviate the need for stringer support.
To guide what follows, I'm going to also propose a few motivating examples which any design should support well.
I would add a Result[T] type (an error or a T) to that list, as well as a Stringish (fmt.Stringer or string) to it.
I'm also not sure about the Direction example. It is true that Rust and Haskell do it like this. I think it's not a bad thing to be able to do it using sum types. But I don't think it should necessarily be a priority. Because we could do sum types and C-style integer enums. And the latter could have some advantages.
Then again, we need to carve out some space for unit terms anyways, to support Maybe[T].
We'll call a sum type a "union" in the following, but I'm not wedded to the name
Please don't. There are fundamental differences between unions and sums. The "discriminated" in "discriminated union" is important and makes it something else entirely.
I'm not super opposed to calling it "union" ultimately, but for the discussions, it would be really helpful, if we continued to keep these straight. In particular, in my opinion, if we do unions, we should do them like #57644 (I maintain that that issue is mistitled). If we don't do #57644, we should do sums.
What you describe are sums, which is good and clearly distinguishes it from #57644.
The zero value of a union is the zero value of the first variant […] Yes, this means the order of the variants in the declaration is important.
I agree that it should work like this. I want to point out that the only reason we couldn't use the order for #57644 is because it reuses the commutative | syntax. If we introduce a completely new syntax, making the order relevant is completely unproblematic.
m.Unset = atom{}
Side-note: if we introduce the predeclared identifier atom, it would be possible to allow its use as either type or value. That is, it would be possible not to have to write atom{}.
How would it handle
fmt.Stringer?
@neild has not talked about a reflect API, but I would imagine that being used. That is, the fmt package would stringify "unions" by looking up which variant it contains via reflect and then stringifying that (e.g. by either printing the name, for atom, or a composite literal, for others).
That's how it works for other composite types. structs or []T don't get a String method either.
I feel like a lot of the special cases can be avoided by removing atom and making value-less variants distinct types with struct{} as their underlying type. Direction.North is a type and Direction.North{} is its singleton value. Value-carrying variants are simply type aliases.
type Direction union {
North, South, East, West
}
d := Direction.North{}
switch d {
case Direction.North{}:
case Direction.South{}:
}
_, ok := d.North // ok is true
I would add a Result[T] type (an error or a T) to that list, as well as a Stringish (fmt.Stringer or string) to it.
fmt.Stringer-or-string sounds more like a type constraint to me than a sum type. Can you give an example of how you see it being used?
@neild Every type that is useful as a constraint, is useful as a type in a function that only has one parameter. Either way, I think it is useful to have a representative of having interfaces as terms in a sum (which is something #57644 doesn't handle well). Result[T] is similar in that regard (if you consider it as union { Value T; Err error }), but it's more specific and people might be tempted in arguments to special-case it. Or exclude it outright, on the basis that Go has multiple returns. Though to be clear, I think Result[T] is useful even with multiple returns, e.g. as an element in a channel.
Treating struct{} differently is going to be confusing [...]
The motivation for treating struct{} differently is to better support data-free variants, like "North" or "Unset".
I think supporting data-free variants well is important. Taking Rust as an example of a popular language with good support for sum types, Rust enums not only have excellent support for data-free variants but the one enum type considered so important that it is defined as part of the language (Option) makes use of the feature.
Perhaps we can do a good job of supporting data-free variants without so many special cases, however.
One question is whether a data-free variant has a type. Let's consider an Option[T] type (using Rust's naming) with two variants: None (no data) and Some (type T). We can either say that the None variant has no type at all, or that it has a zero-size type. In Go, we generally write the zero-size type as struct{}.
Defining the None variant as having a type of struct{} is simpler in the type system than saying that it has no type at all, since it lets us say that all variants have a type. Let's keep that for now.
Everything else is syntax.
Let's also keep the type alias type atom = struct{}.
type Option[T] union {
None atom
Some T
}
We can initialize an Option[T] variable with a composite literal:
some := Option[int]{Some: 42} // this is fine
none := Option[int]{None: atom{}} // this is too cumbersome
@Merovius points out that we could allow the symbol atom to be used as a type or value. That's a bit weird, but it would slightly simplify the second case:
none := Option[int]{None: atom} // Better? But still verbose and probably confusing.
Let us keep a special case here, then: The selector expression T.f on a union type T will evaluate to T{f: atom{}} if the type of f is atom. The expression is an error if f is any other type, including a named type with an underlying type of struct{}. (We disallow named struct{} types to avoid the problem @ianlancetaylor points out above about adding a field to an empty struct{} becoming a backwards-incompatible change.)
none := Option[int].None
some := Option[int].Some // error: Some is not an atom
I think that looks quite nice and has a clear meaning.
Another possibility which would avoid the special case for atomic variants is to say that:
* T.f(v) is equivalent to the composite literal T{f: v}
* T.f() is equivalent to the composite literal T{f: zeroValueOfF}
none = Option[int].None()
some = Option[int].Some(0)
// Valid and equivalent to the above:
none = Option[int].None(struct{}{})
some = Option[int].Some()
That reads pretty well, but I think requires a new type of expression: () in an expression is currently a call or a conversion, but Option[int].None() is neither of these.
Moving on, let's consider access. We can drop the special case for atomic variants entirely, saying that the selector expression u.f on a value of union type u always yields two values:
var option Option[int]
x, ok := option.None // the type of x is atom (struct{})
y, ok := option.Some // the type of y is int
x := option.None // invalid
y := option.Some // invalid
if option == Option[int].None {
// variant none is selected
}
This is a bit more verbose than if option.None { ... }, but it's not terrible.
We don't need special cases in the remaining syntax (assignment to a selector, and switches):
var option Option[T]
option.None = atom{} // probably most people will write: option = Option[int].None
switch v := option.(union) {
case None:
// v has type atom
case Some:
// v has type T
}
To summarize: I think we could drop almost all the special cases for atomic variants with little impact, except for the special case for selectors on types. We could drop that special case as well with an additional syntax change:
// With a special case for selectors on types:
none := Option[int].None // valid
some := Option[int].Some // error
// With a syntax change:
none := Option[int].None()
some := Option[int].Some(42)
zero := Option[int].Some() // valid, same as Some(0)
I do broadly like the idea of using a call-like syntax for constructing values of a union type instead of using the composite literal syntax, but I feel torn about one specific detail: omitting the parameter to get the zero value of the variant's type.
Option[int].Some() being valid and equivalent to Option[int].Some(0) seems like an opportunity for mistakes that are difficult for humans to notice and debug. If that were the design then I'd like for there to at least be a vet rule which recommends using the no-arguments form only for struct{}-typed variants, and using the one-argument form in all other cases.
(I would prefer this rule to be part of the language, but I do appreciate the argument that it's annoying to special-case struct{} as part of the specification of this feature.)
@Merovius points out that we could allow the symbol atom to be used as a type or value. That's a bit weird […]
FWIW there is one precedent for the predeclared identifier nil being used as a type (and obviously it can be used as a value). Not saying that it's not a little weird, but I don't find it that weird either. But I don't think it's likely we'll do it, either way.
go if option == Option[int].None { // variant none is selected }
One consequence of the design is, that while that works, this would be invalid:
if option == Option[func()].None {
// variant none is selected
}
I find that a bit strange.
@neild has not talked about a
reflectAPI, but I would imagine that being used. That is, thefmtpackage would stringify "unions" by looking up which variant it contains viareflectand then stringifying that (e.g. by either printing the name, foratom, or a composite literal, for others).
I have not specialized in compilers so I could be wrong but it seems like to me that the compiler could easily see the code type Direction union { North, South, East, West }, recognize that no String() method is declared, and generate code for a String() method automatically, no reflection required.
I would see that as a pragmatic helpful solution to address the 80 percentile use-case and one that could easily be overridden when needed, no reflection required AFAICT?
And correct me if I am wrong, but if no code uses the String() method Go will not include it in the compiled executable, right?
That's how it works for other composite types.
structs or[]Tdon't get aStringmethod either.
I think it would be hard to come up with a reasonable default for String()forstructs or[]T` but I don't think the same could be said for this "union" proposal.
BTW, auto-generating String() would be the type of solution I was said I found the early pragmatism of Go appealing compared with always being constrained to purely orthogonal solutions. But I get it, those days are gone. Does not mean I can still lament that fact, though.
generate code for a String() method automatically
There are no cases in the language where we generate methods automatically. I can't see that ever changing at this point.
There's also no need for it in this case. fmt.Print(struct{X, Y int}{1, 2}) prints "{1 2}" despite there being no String method on the printed value. The fmt package can handle unions in the same way.
it seems like to me that the compiler could easily […]
My assumption that we'd use reflect was not based on feasibility or difficulty, but on precedent.
BTW, auto-generating String() would be the type of solution I was said I found the early pragmatism of Go appealing compared with always being constrained to purely orthogonal solutions. But I get it, those days are gone.
Using a pre-existing mechanism to do the thing - instead of introducing a new, bespoke mechanism - is the pragmatic choice. Especially if that mechanism is already used everywhere else in the language. And formatting values was one of the (if not the) main use cases reflect was added to the language for.
I find this display of nostalgia misplaced.
Using a pre-existing mechanism to do the thing - instead of introducing a new, bespoke mechanism - _is the pragmatic choice._
To claim there can only be one view of what is pragmatic or what is appropriate nostalgia is a substantial conceit.
But, to continue to discuss this would be to hijack the discussion, so I will not beat this dead horse any further.
Defining the
Nonevariant as having a type ofstruct{}is simpler in the type system than saying that it has no type at all, since it lets us say that all variants have a type. Let's keep that for now.
I like this proposal in general (now that I understand it better) but I think this is where I disagree. I agree with your arguments that if we go down this road we should support union variants with no data. But I think that rather than special casing unnamed struct{}, so that we can have variants with no data actually have data that happens to be zero-sized, let's just go all the way and support union variants with literally no data.
It's basically the same as the proposal here, except that you can only write Direction.North, you can't write Direction.North = struct{}{}. == and .(union) work as expected. dir.North is a single untyped boolean value that reports whether dir contains the North variant. Same for maybe.Unset, whereas maybe.Set evaluates to a value and an untyped boolean (I actually think it should be permissible to use maybe.Set in a single-value context that panics if maybe does not contain the Set variant, but that is a separate discussion).
In other words I think everything is just as you propose except that we don't get caught up in special cases for struct{}. We instead get caught up in special cases for untyped variants, but I think that is OK because that is what we want: we want untyped variants to be special, and it's easier to do that if we start with them being untyped than it is if we start with having them have a particular type that we then treat specially.
The only question then is how to indicate an untyped variant, if we aren't going to use struct{}. I happen to like iota but as it can be a predeclared identifier it can really be anything at all.
I feel like a lot of the special cases can be avoided by removing
atomand making value-less variants distinct types withstruct{}as their underlying type.Direction.Northis a type andDirection.North{}is its singleton value.
I agree with @neild that doing so is a different proposal, but it is very tempting to me to explore. For example:
func (Direction.North) Move(...) {...}
func (Direction.West) Move(...) {...}
...
...
d := Direction.North
pos = Move(d, pos)
...
The are other avenues towards exhaustion here that I'm not sure I completely grasp. Rust has a fairly rich set of syntactic forms, some influences from other languages, accumulated over time and experience, etc. - this proposal feels like just a start in that regard. Again I'd agree with @neild that "[e]verything else is syntax" ... it seems to me like there might be a fair amount of syntax to work through, to get a very polished language feature. (Any discussion about Option[T] with exhaustion, or things along these lines are worth having, IMO.)
Aside, I very much like the approach to zero values in this proposal!
The only question then is how to indicate an untyped variant, if we aren't going to use
struct{}.
How about union{}?
Direction.North is a type
This is something I want to avoid.
In type theory, we might view the fields of a struct as types. In the Go type system, however, struct fields are not types. I feel that sum types in Go (if we add them) should be as similar as possible to our product types (structs), which means variants should have a type rather than being a type.
How about union{}?
That's a sum type with no variants. We want a way to define a variant with no data.
How about union{}?
That's a sum type with no variants. We want a way to define a variant with no data.
But aren't we looking for something that can hold 0 values (union{}) instead of something that can hold 1 value (struct{} with value struct{}{})? I thought that was the core of @ianlancetaylor 's consideration.
But aren't we looking for something that can hold 0 values (union{}) instead of something that can hold 1 value (struct{})?
Ah, I see your thinking.
Both union{} and struct{} are types where there is only one possible value for the type (union{}{} and struct{}{}, respectively).
Both
union{}andstruct{}are types where there is only one possible value for the type (union{}{}andstruct{}{}, respectively).
I don't think that union{} should have a value union{}{}. union{} should be ⊥ (a.k.a. "void" in some languages).
The number of values should mirror familiar identities:
I don't think that union{} should have a value union{}{}
If you can write var u union{}, then u must have some value. There's no such thing as a variable without a value.
(We could disallow empty unions, but that seems like a needless complication.)
(We could disallow empty unions, but that seems like a needless complication.)
Alternatively, we could permit var u union{} while ensuring it has no constructors and no eliminators, making any such variable uninhabited and unobservable. From a type theory perspective, this would be the correct approach.
@neild —
There's also no need for it in this case.
fmt.Print(struct{X, Y int}{1, 2})prints "{1 2}" despite there being noStringmethod on the printed value. Thefmtpackage can handle unions in the same way.
Interesting point.
However, being able to use fmt.Print() to print the name does not satisfy the fmt.Stringer interface so it doesn't allow it to be assigned to a variable are passed as a parameter that needs that interface. Realistically that use-case is of more interest to be then being about to just print a label.
OTOH I now accept that boilerplate will always be required no matter which implementation of unions/sum types is chosen, so just pointing out this distinction for clarity of intent.
If we're going to support union members that have no data at all rather than having zero-length data then I think I've come around to it being okay for that to just be a name with nothing after it at all:
type Option[T any] union {
None
Some T
}
I acknowledge the reservation that this is the same syntax shape used for embedding in a struct type, but I don't think that particularly matters because it doesn't seem like embedding is a reasonable thing to expect a union to support. What would it even mean to embed a type in a union in that way?
Each kind of type has its own declaration syntax tailored fit the needs of that type kind. They are similar at a high level, but inevitably different in the details.
I find the above example both concise and clear. It seems like the most direct possible description of what I was intending to describe. It's syntactically distinct in a similar way to how the assignment and access syntaxes are syntactically distinct for these no-data variants: it mentions only one thing vs. two things.
Alternatively, we could permit
var u union{}while ensuring it has no constructors and no eliminators, making any such variable uninhabited and unobservable. From a type theory perspective, this would be the correct approach.
In previous similar proposals we found this to be infeasible. There are various features of Go that assume that every type has at least one value called its zero value:
Previous discussions did not find any satisfactory solutions for allowing uninhabited types to exist in Go.
What would it even mean to embed a type in a union in that way?
I would expect it to mean inclusion in the sum. That is, I'd expect union A { X int; Y string }; union B { A; Z float64} to mean the same thing as union B { X int; Y string; Z float64 }.
I like the syntactic simplicity of having "no type specifier" mean "no data". But I think I would actually expect embedding with unions to work, given that it works with other composites.
When I said "what would it mean" I suppose I was thinking in terms of practical usage rather than theory.
The typical reason to embed a type into a struct is to create the effect that fields and methods of the embedded type can be used as fields or methods of the containing type.
But as far as I can see that would not be possible for a union type. The methods and fields of the inner type are accessible only when the corresponding variant is selected, and we'd need to use one of the two assertion syntaxes to conditionally access that member.
I can see from your example that you are interested in embedding one union inside another, and I assume you mean that all of the variants of the inner type would be accessible as if they were direct variants of the outer type. I do agree that this specific combination could work and could be useful, but I do think it's valid to ask whether that pattern is important enough to make the arguably more important case of data-free variants have cluttered syntax with an additional boilerplate keyword. 🤷🏻♂️
I don't mean to say it's definitely wrong, just that I think it's a question to be discussed rather than immediately assuming an answer. None of the use cases that were given to motivate this proposal suggest that embedding is more important than ergonomic data-free variants, but maybe there's an example we didn't discuss yet.
In type theory, we might view the fields of a struct as types. In the Go type system, however, struct fields are not types. I feel that sum types in Go (if we add them) should be as similar as possible to our product types (structs), which means variants should have a type rather than being a type.
type T struct {
F string
}
type F T.F // T.F is not a type
It's true that struct fields are not types, but it would be a relatively small change to the spec to allow this.
It 's true that struct fields are not types, but it would be a relatively small change to the spec to allow this.
While I agree that it's possible in principle, I'm skeptical that items a "relatively small" change.
For example, it interacts with the generics features if it'd be possible to use a field of a type parameter as a type: that seems like it effectively introduces "associated types" into the language through a backdoor.
That's not necessarily a bad thing, but not obviously a good thing either. If you are interested in discussing that then I think it should be a separate language proposal, to allow for discussion about details like that to happen in a separate issue.
When I said "what would it mean" I suppose I was thinking in terms of practical usage rather than theory.
It is strictly more practically useful than embedding interfaces. With interfaces, all it does is save you some typing. With sums, you get distinct types that you can use individually - or together, depending on need:
type Length union {
Meter int
Feet int
}
func (a Length) Rect(b Length) Area
type Area union {
SquareMeter int
SquareFeet int
}
type Unit union {
Area
Length
}
func ToMetric(v Unit) Unit
Seems like a practically useful facility to me.
The typical reason to embed a type into a struct is to create the effect that fields and methods of the embedded type can be used as fields or methods of the containing type.
The comparison with embedding interfaces would make far more sense; structs are products, while both interfaces and sums are a form of union.
I was thinking this morning about other things this design could possibly do. One possibly-interesting consequence of this design:
type MaybeUnexported[T any] union {
unexported T
Exported T
}
I don't know is this has any practical use, but this design allows a mix of exported and unexported variants, and presumably only code in the same package could name the unexported ones in order to union-assert or populate them.
Of course it's already possible to do stuff like this with unexported implementations of an exported interface, so I think this is just a new way to write it rather than a truly new capability.
@apparentlymart That would provide a cute hack to force users to put a default case into switches: every switch must be exhaustive or have a default and it can't be exhaustive. Similar to how unexported fields force keyed struct literals. Could be useful for cases where you want to be ensure you're able to add to unions over time without breaking consumers.
In that case, you'd probably spell it union { _ struct{}; /* exported terms */ }.
I wonder if there's some tension between embedding and inferred zero values. For a zero value of Unit, with embedded Area and Length, by the "first appearance" rule we have to glance at Unit. Is it always a reasonable rule? For example, in a package organized around providing SI units, I think "first appearance" might want to indicate invalid construction. And there's some questions about nesting, where one might be interested in nesting (Lenth, Area) as Distance metrics, etc.
It seems like there are design choices that make tradeoffs between small-cardinality stuff - typically for control flow edges, Maybe/Either/Option/Result etc. - versus high-cardinality stuff - typically emerging in domain-specific settings. I really, really like the zero-value-by-order-of-appearance rule for the former. It's a very smart idea for the small-cardinality use cases. I wonder about high-cardinality, or nested usage - and whether there are conventions or syntax or a little more machinery to think about.
For example, in a package organized around providing SI units, I think "first appearance" might want to indicate invalid construction.
You can choose to do that, with the (recursively) first member rule:
type Unit enum {
Zero atom
Length
Area
}
Or maybe even, given what we talked about above:
type Unit enum {
_ atom
Length
Area
}
And there's some questions about nesting, where one might be interested in nesting (Lenth, Area) as Distance metrics, etc.
I mean, yes, ISTM that's a pretty golden variety API design/modeling question? As long as you have the tool to decide that however you want, I don't really see how this would influence the design of the language.
I'm fuzzy on the problem here. Big picture, what is the problem being addressed?
Can we come up with an "enum problem" that is like "the expression problem"?
Can we articulate short, concise, actual problem that is not well solved presently...
Because right now it just seems like the problem is, "I miss things from Rust when I have to write in Go".
This is an okay problem to have. But maybe missing enums its just one symptom, and the underlying problem might be better approached by improving Rust + Go integration. For instance, make calls between Go and Rust be zero overhead, allow them to refer to each other's exports during compilation, etc.
A keyword addition like "exhaustive", to declare a type such that a type's switches must be exhaustive (or else have a default:) would seem to knock out half of the fuzzy problem space, without too many more overlapping concepts.
I really like this. If you also make it rangeable (e.g. to populate dropdowns), you have a winner.
This seems a little like what I was going to propose! :) I filed a separate issue for it: https://github.com/golang/go/issues/76949. In short, it's this:
type Bit sum {
Zero product{}
One product{}
}
v1 := Bit{Zero: product{}{}}
switch v1 {
case Bit{Zero: product{}{}}:
case Bit{One: product{}{}}:
}
switch v1.(match) {
case {Zero}:
case {One}:
}
type Bool enum {
False
True
}
v2 := Bool{False}
switch v2 {
case Bool{False}:
case Bool{True}:
}
switch v2.(match) {
case {False}:
case {True}:
}
This seems a little like what I was going to propose! :) I filed a separate issue for it: #76949.
switch v2.(match) {
case {False}:
case {True}:
}
Nice innovation, that v2.(match).
It's a long conversation, so I'm not sure I followed it all, but my two cents are I like this color of the bikeshed:
type Direction enum {
Unset iota
N, E, S, W iota
Degrees, Radians float64
}
var d Direction
d == Direction.Unset // true
d = Direction.S
d.Degrees = 180
d.Radians = math.Pi
I mean, yes, ISTM that's a pretty golden variety API design/modeling question?
I think you're right that it's mostly up to API design, and I don't think there's anything formally unsound in the easiest or most obvious things. Just as open commentary, wondering if there are some tweaks:
Often I think this proposal would be very nice for the genuine cases where "this set is exhaustive". Maybe[T] is pretty point-blank, semantically uncomplicated. A zero None works great.
Most generally, we're always in the domain of Go's earliest design decisions, the runtime, the systems it runs on, the systems that build it, etc., and even before meeting other domains this may lead to a lot of "This set is exhaustive, but..." cases:
So I would anticipate there is some subtlety and nuance in the intent behind choices about zero values that will, at times, be hard to be completely sharp about. API design should be pretty sensitive to this, but to flip it around - do API designers have enough flexibility, as much as possible to help offer clear designs?
An example:
type A union {
Undef atom
A1 atom
}
type B union {
Undef atom
B1 atom
}
type U union {
A
B
}
Every time we use U exhaustively, we should be forced to consider the U.B.Undef case. But it's the kind of tension I'm thinking will often present itself - it's not immediately clear, structurally, if or why U.B.Undef is different from the U.A.Undef case. Is it one of the "exhaustive, but..." cases? Is it something more domain-specific? I worry that despite the impression that exhaustive enumeration is clearer or more resilient, it can actually be not helpful for using U. We also don't have much pattern matching (and I'm not sure we'd want to go down that road) as mitigation here.
A couple of ideas:
(1)
A universal nil that folds into one case for purposes of exhaustion:
type X union {
nil
X1 atom
}
type Y union {
nil
Y1 atom
}
type XY union {
nil
X
Y
}
...
// exhaustive:
switch xy.(union) {
case nil:
case X1:
case Y1:
}
(2)
or, don't allow the same element to be considered of the same type when embedded:
type Xembed union {
X
}
such that:
x1 := X.X1
e1 := Xembed.X1
x1 == e1 // neither true nor false. doesn't compile, because they're not actually the same type.
Xembed(x1) == e1 // explicit conversion, true
Every time we use U exhaustively, we should be forced to consider the
U.B.Undefcase.
That's a good point about how exhaustive switches should work in the presence of embedding. As I understand it, with the proposal as written, in this case you'd have to write
switch v := u.(union) {
case A:
switch v.(union) {
case Undef:
case A1:
}
case B:
switch v.(union) {
case Undef:
case B1:
}
}
I think it should be fine to write any of
```go
switch u.(union) {
case A:
case B:
}
switch u.(union) {
case A.Undef:
case A.A1:
case B:
}
switch u.(union) {
case A:
case B.Undef:
case B.B1:
}
switch u.(union) {
case A.Undef:
case A.A1:
case B.Undef:
case B.B1:
}
switch u.(union) {
case A.Undef, B.Undef:
case A.A1:
case B.B1:
}
But not e.g.
switch u.(union) {
case A:
case A.Undef:
case B:
}
That is, it seems reasonable to me to allow matching on nested unions. As long as every possible state has one (and only one, otherwise I would probably consider real pattern matching) case covering it. Come to think of it, we might even want to allow that on non-embedded nested unions.
But that's an opinion, yes. This would have to be thought through. I still think allowing embedding is the overall more consistent option. And we should come up with the right rules to make it work, instead of ruling it out.
(If I'm being honest, I think most of this discussion is pretty academic. I'm personally skeptical we can bring ourselves to do this instead of #57644. That is, I think the main obstacle of this proposal is making the decision not to reuse the interface{a|b} syntax. If we think we can make that decision, I feel pretty confident we can figure out a good design for the details of this proposal. Doesn't mean we can't talk about it, of course)
Here is my take on a Sum type implementation. Any thoughts or suggestions?
algebraic-data-types-experimental README
Syntax aside, your approach defines a sum type as a set of named variants where each variant may have an associated data tuple. The use of a tuple rather than a single value distinguishes this approach from other proposals I've seen for Go. Rust and Swift use a similar approach.
type Shape enum {
Circle(float64)
Rect(float64, float64)
Point
}
Swift:
enum Shape {
case circle(Float64)
case rect(Float64, Float64)
case point
}
I don't believe there are any other types in Go which contain tuples in this fashion. The inability to name individual parameters seems like a source of confusion: In the above example, a rectangle is defined as two float64s, but we don't know which is the width and which the height.
I think that it is more consistent with the rest of the language for variants to have a single associated value. If a variant requires a complex value, defining an type describing that value is slightly more verbose, but clearer and more consistent with the rest of the language:
type Circle struct { Radius float64 }
type Rect struct { Width, Height float64 }
type Shape enum {
Circle Circle
Rect Rect
Point
}
If there is value in tuples, I think they should be added as a separate, first-class feature (as proposed and retracted in #64457) rather than as part of a sum type design.
I do sort of wonder if there's an elegant way to pull apart the enum-as-number part of this proposal from the enum-as-sum-type part of the proposal.
Thinking about how you'd do direction now, it would probably be something like:
type Direction interface {
direction() // unused private method to close the set
}
type OrdinalDirection int8
const (
North OrdinalDirection = iota
East
South
West
)
func (_ OrdinalDirection) direction() {} // just for interface inclusion
type Degrees float64
func (_ Degrees) direction() {}
// etc for Radians and so on
The union type proposal does two things: it lets you close what types implement Direction (allowing for possible compiler optimization and nicer zero values) and close the members of OrdinalDirection (so you don't have to write out the iota and letting the compiler optimize its storage into however many bits are needed).
What if the compiler could detect that Direction has a limited set of types that can implement it? Then the only advantage of the proposal for enum-as-sum-type is that you can pick the zero value. Maybe there could be a proposal to just allow picking zero values?
Optimizing iotas automatically would be convenient, but doesn't strike me as so convenient by itself as to make obsoleting existing Go code worth it. There would be a lot of proposals for going back and making v2s of APIs that use enums instead of their current manual iota based enums, and that feels like churn to me.
interfaces are not the answer of everything. Interfaces define behaviour.
sum types are about types. let's not make the same half-measure implementation like it was for generics where a clearly "type related" operation can not be done (at least not yet) because it was defined based in interfaces.
doesn't strike me as so convenient by itself as to make obsoleting existing Go code worth it....
that feels like churn to me.
A progamming language is a living thing. Backward compatibility aside, churn is not the worst thing that could happen if it makes the code more clear and harder to "hold it wrong".
What if the compiler could detect that Direction has a limited set of types that can implement it?
type EvilDirection struct {
yourpkg.Direction
}
https://go.dev/play/p/FhXc8mk8DAo
EvilDirection can come from a plugin as well, so even whole-program analysis is not sufficient. The entire interface type and all of its implementations would also have to be unexported. And then, where does your documentation go?
I don't think embedding changes the ability to close a set for function parameters. I think you could look at a function that takes a Direction and see if it only asserts to known types and then use the optimized version or if it asserts to other types and then do the unoptimized interface packing same as ever. Globals and slices would be harder though, because there I'm not sure you could analyze where it's being used. Maybe there could be a new magic struct in the structs package.
think you could look at a function that takes a Direction and see if it only asserts to known types and then use the optimized version
But that's the point. You can't, because if you only assert on known types, you are missing some, which changes the behavior of your code. The compiler must never assume that it knows all types that could be passed as an interface. If you write
func F(d Direction) string {
switch d.(type) {
case BasicDirection:
return "base"
case Degrees:
return "deg"
}
}
Then a) this will not compile (because it is missing a terminating default statement) and b) if you add a panic("foo"), that panic will get triggered when called with EvilDirection.
This is leaving aside the point that the compiler needs to know how this function is called only based on the signature (it could be stored in a variable and so has to be callable dynamically), so you inherently can't make the representation of a Direction argument depend on the body.
FTR, I also think it is just fundamentally wrong to say that even if we could effectively close interfaces, proper sums wouldn't have other advantages. Even just the fact that they are documented as sums is a benefit. And they can have methods on their own, which interfaces can't (you'd have to duplicate the implementation on every concrete type).
How do I declare an anonymous union in this proposal? Or am I just to using tagged union?
@Arkissa What do you mean by "anonymous union"? I would interpret that as union{ A X; B Y }, which I assume this proposal would allow (and in Go would likely be called a "union type literal", just like I would call struct{ A X; B Y } a "struct type literal").
I personally would use the choice naming, uniformly require types in both creation and usage (no special-casing of struct{}; call syntax might be nice but we could also use Union{Variant: value}), and ban no-variant choices/unions as they cannot exist in Go (Go fundamentally requires a specific, namable, value of every type, the zero value).
I wonder if the variants of a union type can be used as standalone type?
Example (the syntax is just for illustration):
// Define x to be of type Maybe.Set
var Maybe[int64].Set x = Maybe.Set{12345}
// This is illegal. Cannot change x's type.
x = Maybe.Unset
// Suppose we have func myFunc(x Maybe[int64].Set).
// Then this is legal ...
myFunc(x)
// ... but this is not
myFunc(Maybe.Unset)
// And y has union type
var Maybe[int64] y = x
// then this is illegal ...
myFunc(y)
// ... unless user proves that y is set
if v, ok := y.(Maybe.Set); ok {
myFunc(v)
}
This should make it less verbose to use union types, if the variant type can be proved at compile time.
I'm not sure how useful this design is, since obviously one can always just pass the "inner" value (in this example, the int64 12345) around, instead of wrapping and passing it as a Maybe. It might only be necessary for the enum cases, like Direction.North, which doesn't have an inner value.
FYI, wrote a lengthy comment on [#57644](https://github.com/golang/go/issues/57644) on another advantage of this proposal: it naturally allows generic methods on sum types, if #77273 is accepted. See the linked comment for details.
I will admit to not having fully digested this thread yet, so this particular mechanism might have been discussed, but it seems like there's some amount of agreement that reversing this decision fixes a lot of problems:
Q: How about
m := Maybe[int].Set(42), as opposed toMaybe[int]{Set: 42}?I considered extending type conversion syntax for creating a union variant in this fashion. I don't like it, though, because it makes the variant look like a type. One of my notions in this design is that while in programming language theory each variant may be a type, expressing those types in the Go type system just adds confusion.
One way to justify this is to distinguish between the notion of a type and the notion of a constructor. In Go to date, these map 1:1, but they split in other languages. Go has historically rejected them in order to avoid allowing them to have behavior, but this doesn't do that.
So for instance:
type foo struct { ... }
type bar foo // bar is a type
var x = bar{...} // bar is a constructor; this is syntactic sugar for the following:
var y = bar(foo{...}) // bar is a constructor
In this view, we have
type Maybe[T any] union { // Maybe[int] is a type
Unset atom
Set T
}
var x Maybe[int] = Maybe.Unset{} // Maybe[int].Unset is a constructor for Maybe[int]; this is sugar for the following:
var y Maybe[int] = Maybe.Unset(atom{})
var z Maybe[int] = Maybe[int].Set(42) // Maybe[int].Set is a constructor for Maybe[int]; we might allow int to be inferred here
var w Maybe[int] = Maybe[int]{Set: 42} // ERROR! Maybe[int] is *not* a constructor!
This requires us to write d == Direction.North{} instead of omitting the empty braces, but there's no confusion about what's a type and what's a value.
I've been reading this proposal and all comments. In short: it looks good to me!
Despite an intimidating number of comments in this issue, I think they've essentially probed the idea, and after all the probings, the result seems to be that it's still sound.
I've yearned for sum types in Go for a long (long) time, and this looks like a very viable way to get a very valuable new form of expressiveness (and restraint that improves correctness) into Go. I'd love to see it move forward.
I have several more detailed opinions, which I'll share below, but all of the following are in the range of "nits", "imho", and bikeshedding of a type I'd encourage you to freely ignore. The overall opinion I have of this, as it stands from the very first post, remains "LGTM; solid; let's go".
All the agreements first (of which there are many):
.?" syntax in the future would probably be a good way to address it; such coalescing syntaxes in other languages has seemed well-regarded everywhere I've seen them introduced. But, that's a further topic and doesn't need to be hitched to this proposal.)struct{}{} and union{}{} would be two distinct ⊥ types. I think that's... fine? A little odd, but fine. Or: Declaring that struct{}{} == union{}{} also seems acceptable to me if that results in simplifications in the implementation.A few nits:
Direction{North} syntax (where the value of atom{} can be implied); it's a syntax I would've intuitively expected to work. But I'd agree it's not required when similar brevity is available through Direction.North syntax.iota being reused for this, however. Iota is about counting up; a series of empty types are not counting; and nonempty types aren't disrupting any count.)atom as a value. E.g., Maybe[T].Unset = atom (no additional braces). I could also understand a position that this is too much of a syntax irregularity. But I think it would be nice. (Merovius had a similar observation.)struct{} should be forbidden as union member types. That seems surprising. I think one may end up wanting to do that as a program evolves and being blocked from it would confuse me upon discovering it.Maybe as Some and None, as is typical in other languages and libraries. Or at least: not having one of them be called Set. I find Set to be very sabo-in-the-mental-gears because it also pattern-matches as the imperative mutation word and with the collection type, both of which are very far from the desired concept here.And some follow-on wishes:
Result[T, E]. It might also feel like a gopher normal thing to do to hardcode E to error, but my feeling is that that would close a lot of doors very permanently, for very little gain in perceived simplicity, and would thus be a poor choice.The gnarliest thing I saw, in all the comments:
Also, in case these haven't been seen or surveyed for ideas and proof of practicality, here are two other projects that are golang-entangled and have added sum types:
I've played with both, and the ability to use sum types within them is very pleasing. And both have introduced these syntaxes and semantics without catastrophe! So I think that speaks very favorably of the possibility for Golang to manage this, too.
Thank you to everyone working on refining this, and hopefully carrying this or one of the other sum type proposals closer to reality!
I think it's implied that both
struct{}{}andunion{}{}would be two distinct ⊥ types. I think that's... fine? A little odd, but fine. Or: Declaring thatstruct{}{}==union{}{}also seems acceptable to me if that results in simplifications in the implementation.
⊥ has no values (definitionally) but struct{} has exactly one value (struct{}{}), so I don't think struct{} is a ⊥ type. But union{} would be. However, skimming the union{} discussion again:
[proposal text]
The zero value of a union is the zero value of the first variant[discussion]
If you can writevar u union{}, thenumust have some value. There's no such thing as a variable without a value.(We could disallow empty unions, but that seems like a needless complication.)
Not being able to write var u union{}, which has no well-defined zero value, seems like a pretty critical need for this complication. I feel pretty strongly, that union{} should be prohibited as an uninhabited type.
I'd prefer that to be a
Result[T, E]. It might also feel like a gopher normal thing to do to hardcode E to error, but my feeling is that that would close a lot of doors very permanently, for very little gain in perceived simplicity, and would thus be a poor choice.
Personally, I don't think this is practical. We live in a world in which a lot of existing Go code returns error. It would be far too easy to do something like
type MyError struct{}
func (*MyError) Error() string {}
func G() Result[atom, *MyError] {
return Result[atom, *MyError]{}
}
func F() error {
switch v := G().(union) {
case Result.Value:
case Result.Error:
return v // returns a non-nil error that has a nil *MyError
}
In my opinion, the one practical solution to the most common nil vs. nil confusion is to strongly advocate to only ever pass error over API boundaries. Whatever you think of error, it is spread in every Go codebase and it's a design we're stuck with.
[edit] Actually, I just realized that this doesn't really make sense. The point behind Result is that the Result.Error branch is only taken, if there was an error. So as long as it's defined as type Result[T any, E error]{ Value T; Error E }, that nil-problem couldn't happen (unless someone explicitly returns Result[T, *MyError]{Error: nil}, which could be reasonable). So, I think I could live with that.
@ianlancetaylor : The only question then is how to indicate an untyped variant, if we aren't going to use
struct{}. I happen to likeiotabut as it can be a predeclared identifier it can really be anything at all.
Does the syntax for union require a symbol like iota, or can lack of a symbol indicate a variant is untyped? It would be nice to avoid the jargon if possible.
type Direction union {
North, South, East, West
}
type Maybe[T any] union {
Unset
Set T
}
===
Edit: I see this was considered in the original post ( see below). Are embedded unions part of this proposal? If so, I suppose union { Unset; Set T } indicates Unset is an embed. If not, there is no ambiguity, and it is more a style question. I would personally vote for no keyword like iota, and if one is needed, picking an English word with more direct meaning like untyped or empty.
However, I am not sure I follow the point about map[string]atom below. Why is that favored over map[string]struct{}? I guess it is because, as originally stated, "We use atoms to indicate variants that have no data. The new identifier is introduced because it looks weird and hacky to write struct{} all over the place and struct{}{} is worse." Introducing a language-level type <some keyword> = struct{} seems like a reasonable proposal on its own that could possibly be debated independently.
FAQ
Q: Do we need this atom type? We could just omit the type from the union definition for the "has no value case".
That works, but it looks a lot like an embedded type:
type Maybe[T any] union {
Unset // looks like an embedded Unset type
Set T
}
Also, atoms are pretty nice for non-union uses, likemap[string]atomorchan atom.
With that shorthand,
type Direction union {
North, South, East, West
}
would be functionally identical to
type Direction union {
North; South; East; West
}
right?
But to write the Maybe in one line, it must be
type Maybe[T any] union {
Unset; Set T
}
because using a comma would give Unset the type T.
(In practice, I would expect gofmt to replace the semicolons with linebreaks.)
This feels kind of inconsistent with embedding types in structs, though.
_Edited to add: rewritten and simplified version, based on comments, in https://github.com/golang/go/issues/76920#issuecomment-5310309424._
I'm going to restate this proposal using different keywords and syntax. The ideas are all from @neild, I'm just rearranging it in a way that seems to me to have some advantages.
struct selectWe introduce a new kind of struct type, a struct select. This is what other languages call a "union" or "sum" or "enum" or "variant" or "oneof" type: a type that can contain a single value that must be one of a list of types. We use the term struct select to avoid introducing a new keyword. (A new keyword would not be a backward compatible change, as it would break existing Go code that uses that keyword as the name of a type. While we could choose to break existing code, avoiding that is preferred if possible. select is already a keyword in Go, so using it in a new syntactic position poses no compatibility issues.)
Syntactically a struct select type is defined like a struct type:
StructSelectType = "struct" "select" "{" { FieldDecl ";" } "}" .
type IP struct select {
IPv4 [4]byte
IPv6 [16]byte
}
We will call the elements of a struct select variants. Each variant has a name and a type. A value of type struct select must contain exactly one value, whose name and type must be those of a variant of the struct select. The zero type of a struct select is the name and zero value of the type of the first variant listed in the declaration. Code may assign to a struct select as though assigning to a field of a struct; the assignment will replace any existing value.
var v IP
v.IPv4 = [4]byte{127, 0, 0, 1}
v.IPv6 = [16]byte{} // v now holds a [16]byte value, not a [4]byte value
The value of a struct select always holds exactly one variant. Referring to the variant stored in a struct select returns that value. Referring to a variant that is not stored in a struct select panics. The comma-ok form may be used to select both a variant and a bool indicating whether that is the variant stored in the struct select, as with a type assertion.
v.IPv4 = [4]byte{127, 0, 0, 1}
x1 := v.IPv4 // succeeds, x1 gets value [4]byte{127, 0, 0, 1}
x2, ok := v.IPv4 // x2 = [4]byte{127, 0, 0, 1}, ok = true
x3 := v.IPv6 // panics
x4, ok := v.IPv6 // x4 = [16]byte{}, ok = false
Values of type struct select may participate in a type switch. Each case of the type switch must be the name of a variant.
switch x := v.(type) {
case IPv4:
// x is type [4]byte
case IPv6:
// x is type [16]byte
default:
// Can't happen in this case, but if it could x is type IP
}
The compiler will issue an error if there is no default case and there are some missing variant cases. This means that adding a new variant is a breaking API change. This may be mitigated with a variant whose name is _. A type switch on such a type can never name all the variants, and must therefore always have a default case.
(If we think it is confusing to use .(type) when the cases are variant names, we could use .(select) instead.)
struct select values may be assigned to variables of the same type.
struct select values may have methods, and may be assigned to interface values in the usual way.
struct select values may be compared to values of the same struct select type using == or !=, provided at least one variant has comparable type. Two values are equal if they have the same variant and if the variant values are equal. If the values are not comparable, the comparison will cause a run-time panic.
Composite literals of struct select type must be keyed and must have at most one key/value pair.
Compilers are encouraged to pack struct select values as tightly as possible in memory. The exact size of a value of type struct select is not specified by the language, except to say that any given type must be at least as large as the largest variant type. The alignment must be the largest alignment of any variant type.
nil variantUnlike struct types, a struct select type may have a variant that is simply nil. No name is permitted. If a struct select has such a variant, then code is permitted to assign nil to the variant, and case nil is permitted in a type switch on the variant, and the variant may be compared using == or != with nil. Assigning a struct select holding nil to an interface variable will set the interface variable to a value with the struct select type with the nil variant; it will not set the interface variable to nil. This parallels assigning a nil pointer to an interface variable.
type Optional[T] struct select {
nil
Val T
}
var o Optional[int] = F()
if o == nil { return false }
fmt.Printf("found value %d\n", o.Val)
return true
iota variantUnlike struct, a struct select type may have variants whose type is simply iota. These variants are names with no associated values. Each such variant defines a set of values referred to as typename.variantName_. These are values of the struct select type with the named variant selected.
type Direction struct select {
North, South, East, West iota
}
var d Direction // d has variant Direction.North
d = Direction.South
if d == Direction.South {
fmt.Println("as expected")
}
switch d {
case Direction.North:
case Direction.South: // this case is chosen
case Direction.East:
case Direction.West:
default:
fmt.Println("can't happen")
}
fmt.Printf("%T\n", Direction.North) // prints Direction
d2, ok := d.(Direction.South) // d2 = d, ok = true
d3, ok := d.(Direction.East) // d3 = Direction.North (the zero value), ok = false
d4 := Direction.North.(Direction.North) // ok
d5 := Direction.North.(Direction.South) // compiles, but panics at run-time
Note that we could decide to permit ordered comparisons, such as < between values with iota variants. They would be ordered in declaration order. A comparison of an iota variant with a non-iota variant would panic at run time.
That completes my rewrite of @neild 's idea. I'll edit this if I missed something.
except to say that any given type must be at least as large as the largest variant type.
I think we can't guarantee that? Since of pointers and GC. That would be nice if this worked that way, but I think it might complicate the initial impl.
Unlike struct, a struct select type may have variants whose type is simply iota. These variants are names with no associated values. Each such variant defines a set of values referred to as typename.variantName_. These are values of the struct select type with the named variant selected.
Couldn't we make this work without the iota keyword? just:
type Direction struct select {
North, South, East, West
}
I also wonder what should happen if we have an _ variant. Or is it disallowed?
type Direction struct select {
_ foo
}
struct select values may be assigned to an interface type if every variant type is assignable to that interface type. Invoking a method on the resulting interface will invoke the method on the current variant.
Can struct select have methods? That above paragraph makes me feel that no, but maybe we could allow methods instead of that behaviour? I think it would be better.
A few clarifiers:
Can we have a struct select literal for an iota variant? If so, what goes in the value slot? Direction{North:_}? Or do we not allow that, and you would have to use Direction.North instead?
What is the type of of x in
var d Direction = ...
switch x := d.(type) {
case North: println(x)
}
Not easy to just disallow it, because there may be other non-iota variant cases that want access to the variant value. Unless we enforce all-or-nothing-iotas in a struct select.
I think we can't guarantee that?
Maybe you are reading it backward? Or maybe I am writing it backward. I'm saying that the minimum size is the size of the largest variant. I'm not saying anything about the maximum size.
Couldn't we make this work without the iota keyword?
For a struct you always have to have a field name and a type. I'm concerned about changing that. I think that if we did change that we would have to drop the nil case, though that would be OK.
I also wonder what should happen if we have an _ variant. Or is it disallowed?
You can write _ iota although I don't know why you want to.
Can struct select have methods? That above paragraph makes me feel that no, but maybe we could allow methods instead of that behaviour? I think it would be better.
Yes, good point. That makes more sense. I will edit that out of my comment.
Thanks.
Can we have a struct select literal for an iota variant? If so, what goes in the value slot?
Direction{North:_}? Or do we not allow that, and you would have to useDirection.Northinstead?
My intent was that you have to write Direction.North. You can't use an iota variant as the selector in a composite literal.
What is the type of of
xinvar d Direction = ...
switch x := d.(type) {
case North: println(x)
}
Not easy to just disallow it, because there may be other non-iota variant cases that want access to the variant value. Unless we enforce all-or-nothing-iotas in a struct select.
Good question. The type of x in that case is simply Direction. I should try to clarify that.
Maybe you are reading it backward?
Yes, potentially 7am is to early for reading 😄, missed that at least part.
I also wonder what should happen if we have an _ variant. Or is it disallowed?
You can write
_ iotaalthough I don't know why you want to.
I was thinking about non-iota variant, e.g.:.
type Direction struct select {
_ foo
}
I don't know why you want to.
Me either, but it could see it being used as non-exhaustive marker.
I see you already mentioned it in the original comment:
This may be mitigated with a variant whose name is
_.
EDIT:
You could also use the _ as an "invalid zero-value" which is common in our current iota enums.
A new keyword would not be a backward compatible change, as it would break existing Go code that uses that keyword as the name of a type.
We could, of course, make a new keyword conditional on the language version.
go v.IPv4 = [4]byte{127, 0, 0, 1} x3 := v.IPv6 // panics
This is an option, but I'm really loath to introduce a new way for innocent-looking code to panic. I think we should at least start with the safer version (you must write x, ok := v.IPv6) and relax the requirement if it proves onerous; we can always choose to permit x := v.IPv6 later, but we can't take it away once it's there.
Unlike struct types, a struct select type may have a variant that is simply nil.
The overloading of nil to mean a zero-value pointer and a zero-value interface (among other things) is already one of the more confusing pitfalls of the language. Do we really want another meaning for it?
We could, of course, make a new keyword conditional on the language version.
True, but that is disruptive for tools and go/* packages, but could be worth, previous discussion https://github.com/golang/go/issues/78940#issuecomment-4314638829 on similar topic.
Regarding breaking changes from adding new variants, I see how this part addresses type switches:
adding a new variant is a breaking API change. This may be mitigated with a variant whose name is
_. A type switch on such a type can never name all the variants, and must therefore always have adefaultcase.
But what about this?
struct selectvalues may be assigned to an interface type if every variant type is assignable to that interface type. Invoking a method on the resulting interface will invoke the method on the current variant.
This sounds to me like I can write
type Variant struct select {
SB *strings.Builder
F *os.File
_ io.Writer
}
var v Variant = something
var w io.Writer = v
But if I then add a non-Writer type to Variant, that's still a breaking change, right?
What about pointers to these types?
type Variant struct select {
Foo int
Bar uint
}
var foo Variant
foo.Foo = 1
ref = &foo.Foo
foo.Bar = 2
*ref = 3 // what happens here?
EDIT: potentially Union variants are not addressable. from the initial comment applies here.
Yes, variants are not addressable so you can't write ref = &foo.Foo (where foo is a union/struct select/sum/whatever).
Same as how you can't get a pointer to a map key/value, and for the same reason.
We could, of course, make a new keyword conditional on the language version.
Sure, but that is hardly a panacea. The goal should always be that people can seamlessly use a new version of Go; their code should simply work without them having to change anything. Of course we do not meet that goal as often as we want to. But it's still the goal.
I think we should at least start with the safer version (you must write
x, ok := v.IPv6) and relax the requirement if it proves onerous
I'm not strongly opposed to that but I think we would immediately see plenty of x, _ := v.IPv6. We can try it and see.
The overloading of
nilto mean a zero-value pointer and a zero-value interface (among other things) is already one of the more confusing pitfalls of the language. Do we really want another meaning for it?
Fair point. It's not essential.
But what about this?
Yeah, that was a mistake. I've changed it. Thanks.
This is an option, but I'm really loath to introduce a new way for innocent-looking code to panic. I think we should at least start with the safer version (you must write x, ok := v.IPv6) and relax the requirement if it proves onerous; we can always choose to permit x := v.IPv6 later, but we can't take it away once it's there.
I'm not strongly opposed to that but I think we would immediately see plenty of x, _ := v.IPv6. We can try it and see.
What if we used x := v.(IPv6) as a syntax for checked access? Same for x, ok := v.(IPv6). Then i believe it is clear. This makes it look less like field access, but union.
@ianlancetaylor You have posted your modified proposal twice, which is a recipe for confusion (I read the first one and made some notes that where obsoleted by edits to the second one), can you delete one of them?
As mentioned, I strongly believe we should add a requirement for a struct select to have at least one variant (otherwise your definition of the zero value is invalid).
I really don't like the tedious name struct select. I understand the concerns about breaking changes. But even then, I'd rather try to make union (or whatever) a pseudo-keyword that is only recognized in places where a type is expected (that being said, I'm not sure this can be made unambiguous). That is, I'd rather carry around such a grammar hack in the spec, than carrying around struct select in code.
I really don't like the tedious name struct select.
I really like struct select. Using an existing keyword makes it feel familiar. I'm sure if you look at Go's existing use of select and compare that to other languages similar constructs, there will be different terms used for the same/similar.
On a side note; I'm really happy to see this issue getting some forward momentum.
What if it was just select?
type foo select {
foo int
bar int8
}
Does not look that bad.
(If we think it is confusing to use
.(type)when the cases are variant names, we could use.(select)instead.)
:+1: for .(select) and :-1::-1::-1::-1: for reusing .(type)
I also think we should use .() for variant selection to differentiate it from always safe field selection as others have said
struct selectvalues may be compared to values of the same struct select type using==or!=, provided at least one variant has comparable type. Two values are equal if they have the same variant and if the variant values are equal. If the values are not comparable, the comparison will cause a run-time panic.
There is enough subtlety around comparability. I would rather the rule be that it's comparable iff every variant's type is comparable. That may make certain cases harder to express but the simpler rule makes it easier to reason about in general. Though it blurs the difference this is still more of a struct than an interface so it should stick to struct rules except where it's absolutely necessary.
Can we use just select instead of struct select? Currently it's only used as the initial token of a statement so it's free to use elsewhere. If you want to write an anonymous type as an expression statement you'd have to use () to disambiguate (but then again the farthest you could go down that path results in an unused value error so it's not a case that would ever really come up in practice). It may be somewhat confusing at first to use it in two different senses based on context but I think, once acclimated, it wouldn't really cause confusion in practice. (@mateusz834 beat me to it!)
I don't see the value of the iota or nil variants. They don't provide a lot over variants with the empty struct type (especially now that we have #12854!) and add a lot of extra rules to remember.
type Direction select {
North, South, East, West struct{}
}
var d Direction // d has variant North
d.South = {}
if d == Direction{ South: {}} {
fmt.Println("as expected")
}
switch d.(select) {
case North:
case South: // this case is chosen
case East, West:
default:
fmt.Println("can't happen")
}
d2, ok := d.(South) // d2 = d, ok = true
d3, ok := d.(East) // d3 = Direction.North (the zero value), ok = false
Admittedly d == Direction{ South: {}} is a bit ungainly but not too bad. You can always define names for values you want to compare often. Maybe you could define Direction.South{} as shorthand. That would probably be useful for variants with large composite literals, but it seems like a lot of extra rules for a little bit of extra niceness.
type Optional[T any] select {
None struct{}
Val T
}
var o Optional[int] = F()
if o == {} { return false }
fmt.Printf("found value %d\n", o.(Val))
return true
(you could also just rewrite that to use the comma-okay form)
re exhaustiveness checking: I've been brought over to the opinion that it shouldn't be handled at the language level. Inexhaustive switch is fine (maybe there are 20 variants but you only need to do something in 3 cases) and an enforced default can be ambiguous (is that case missing because this old code that didn't get the case added or because that variant can also take the default branch?). The definition allows for exhaustiveness which can be enforced/flagged by go vet/gopls while still allowing code to be compiled/tested while you're in the process of updating.
struct select
I'm going to try not to discuss syntax much for now (plenty of time if/when we have a consensus on the semantics), but I really dislike reusing existing keywords here. Overloading the meaning of a symbol makes the language harder to learn, except when there's an clear relation between the varying uses of the name. Nobody gets confused that a + b has different meanings for ints and strings, because there's an intuitive meaning for addition of strings. type T int and switch x.(type) { ... } reuse type in different contexts, but both uses are about types. However, struct select reuses select (a choice between a set of possible channel operations) in a context that has nothing to do with channels. struct switch might be a bit more appropriate--a switch statement switches between values, a struct switch switches between types--but I still dislike it.
If we are to add a new variety of type to the language, let us do it in a way that makes it feel as if it was there all along, not something bolted uncomfortably onto the side at a late time. (One of the great successes of generics, I think, is that they do not feel bolted on--someone coming to Go today could easily believe that they had always been a part of the language.)
I would rather the rule be that it's comparable iff every variant's type is comparable.
+1, I think this is less surprising.
As a general principle, I think use of a sum should always treat it as potentially containing any of its variants. If you know what the variant is, you first assert that fact and then operate on the variant. The only operation on a sum which can panic due to the wrong variant being present should be an explicit assertion. (I am agnostic on whether we should have v := sum.(IPv4) as an assert-and-extract, but I like that it looks like an existing unsafe language construct.)
You have posted your modified proposal twice, which is a recipe for confusion (I read the first one and made some notes that where obsoleted by edits to the second one), can you delete one of them?
Yikes, don't know how that happened. I deleted the one I hadn't edited. Thanks for pointing it out.
I like the suggestion to just use select as a type. It's true that this reuses an existing keyword, but it reuses it in a way that makes sense. At least to me.
I'm fairly sure that this requires a keyword, not any sort of pseudo-keyword, because a type can start with any identifier, since any identifier can be the name of a type. In order to know that we are going to parse a list of variants, we need some sort of syntactic marker. It has to be a keyword or an operator.
I probably should drop both the nil and iota variants. They aren't needed.
We could consider saying that for any select type T, T.V is a value of type T holding variant V with the zero value. If we do that, then given
type Direction select {
North, South, East, West struct{}
}
we can write
x := Direction.North
b := x == Direction.North // b is true
b = x == Direction.South // b is false
switch b {
case Direction.North:
// etc.
}
We could consider saying that for any
selecttypeT,T.Vis a value of typeTholding variantVwith the zero value.
It does make the 0 sized variants much cleaner to deal with. Would it be useful in other situations? It feels kind of strange to special case that, but, otoh, it is both an important and common use. I almost suggested something like that but it was T.V{} so you could use it as a shorthand for variants with composite literal syntax. I abandoned it because it didn't make sense for types like int and I couldn't think of other uses, but this might be the right balance to make it worthwhile.
Another option other than just select is just var (for variant). The only awkward part of select is that I'm not sure how to phrase switch v.(select) { in English (a select switch? seems ambiguous) but switch v.(var) { I would call a variant switch without hesitation.
I believe the people calling for a conditional keyword mean that in the parser when checking for a union keyword instead of writing it like current_token == SELECT_TOKEN you write current_token == IDENT_TOKEN && current_token.Value == "union" so that union (or whatever) is only interpreted as a keyword in that specific context but you can continue to use union as a regular identifier outside the specific context(s) in which it is a keyword. (NB: I've never written a parser with optional keywords and don't know if I really care for the idea personally but I believe that's what's meant).
All else being equal I'd prefer a new (real!) keyword but if not select or var works for me.
You match it with switch,how about use switch:
type Result[T, E] switch
Or
type Result[T, E] struct switch
believe the people calling for a conditional keyword mean that in the parser when checking for a
unionkeyword instead of writing it likecurrent_token == SELECT_TOKENyou writecurrent_token == IDENT_TOKEN && current_token.Value == "union"so thatunion(or whatever) is only interpreted as a keyword in that specific context but you can continue to useunionas a regular identifier outside the specific context(s) in which it is a keyword.
Understood. However, I believe that in Go that would introduce significant difficulties. For example, Go permits writing
v = struct { i int }(x) // conversion to struct type
v = str { a, b } // composite literal of type str (str is a type name)
If we don't have a clear indication that we are parsing a type, we have to potentially look very far ahead before we know what we are parsing (note that a in the second line can be an arbitrarily long expression. That goes against a core principle of Go, which is that it is easy and quick to parse.
In other words, the idea of treating an identifier as a keyword in a specific context doesn't help very much, because there are a lot of different contexts in which a type or an expression are accepted. To keep the parser simple in this case, all type literals (types that are not just an identifier) start with a keyword or an operator.
I suppose if you generalize T.V to require either () or {} you could have
selectType.IntVariant(0) ⇔ selectType{ IntVariant: 0 }
selectType.StructVariant{a: 0, b: 1} ⇔ selectType{ StructVariant: &structType{a: 0, b: 1}}
That let's you refer to struct{} variants easily and provides a cleaner version of the composite literal syntax for variant types. It gets it closer to functional languages which makes each variant a constructor while keeping them more tightly namespaced.
Working through a thought: Let's say the variants of a sum/enum/union/whatever are indeed themselves types. Variant types are assignable to the sum; the sum is assertable to the variants.
type IP union {
IPv4 [4]byte
IPv6 [16]byte
}
This defines types IP (the union), IP.IPv4, and IP.IPv6. Ways to create an IP with a v4 address in it:
// Zero value of IP is the zero value of the first variant.
var ip IP
// A composite literal with type IP.
ip = IP{IPv4: [4]byte{127, 0, 0, 1}}
// Assign a value of type IP.IPv4 to an IP.
ip4 := IP.IPv4{127, 0, 0, 1) // type of ip4 is IP.IPv4
ip = ip4
// Field assignment.
x := []byte{127, 0, 0, 1}
ip.IPv4 = x // exactly equivalent to "c = IP.IPv4(x)"
Extracting the v4 address is exactly like converting an interface to a concrete value:
// Type assertion (checked, or panicking).
ip4, ok = ip.(IP.IPv4)
ip4, ok = ip.(IPv4) // may elide the sum type name
ip4 = ip.(IPv4) // potentially panics
// Type switch. (Could require this to be exhaustive or not, this supports either.)
switch v := ip.(type) {
case IP.IPv4:
case IPv6: // may elide the sum type name
}
Let's consider Direction:
type Direction union {
North, South, East, West unit
}
unit is a predefined type alias for struct{}, as I suggested in my original proposal. (I called it atom there, let's try out unit for size.)
@Merovius noted previously that we could permit the same symbol to serve as the type name and the one value that type can hold:
var u unit // exactly equivalent to: var u struct{}
u = unit // exactly equivalent to: u = struct{}{}
Let us extend that notion to variant types with an underlying type of unit. Direction.North is the name of a type, and also the value of that type.
d := Direction.North
// This is also valid:
var north Direction.North
d = north{}
// I suspect this can't be valid without causing parsing ambiguities,
// but if it's unambiguous it should work:
d = Direction.North{}
// This comment is too small to contain a complete discussion of comparability,
// but this works:
if d == Direction.North {
// ...
}
// We could require this to be exhaustive if we want.
switch d {
case Direction.North:
}
// Direction.North is a type, so we can type assert to it:
if _, ok := d.(North); ok {
}
// Or type switch:
switch d.(type) {
case North:
case South:
case East:
case West:
}
That all looks fairly nice, but there's a problem that I've glossed over:
type Stringish union {
String string
Buffer *bytes.Buffer
}
s := Stringish{Buffer: new(bytes.Buffer)}
// This is a type assertion to the variant type Stringish.Buffer,
// but we want a *bytes.Buffer.
//
// What's the type of buf?
buf := s.(Stringish.Buffer)
I think some fairly nice syntax falls out naturally from treating the variants as types, but we (mostly) don't want to actually create variables of these types. In the above example, we want Stringish.Buffer to be a *bytes.Buffer, not some type with the same underlying type as *bytes.Buffer.
Perhaps that's resolvable by saying that a type assertion or switch on a sum results in not the variant type (Stringish.Buffer) but the type of the variant (bytes.Buffer).
The idea of using a named type whose underlying type is struct{} also to refer to the only possible value of that type seems interesting beyond the scope of this proposal, and could perhaps be proposed separately as an orthogonal language change.
For example, when using context.WithValue it's common to define a named type and a value of that type to use as key, which today tends to require some awkward boilerplate that can be confusing to an unfamiliar reader and renders slightly awkwardly in godoc. e.g.:
type contextKeyType struct{}
var ContextKey keyType
ctx := context.WithValue(parent, ContextKey, 1)
If the language supported using named types based on struct{} to represent the single possible value of that type as a general feature, then the above could be reduced just to the type declaration:
type ContextKey struct{}
// ContextKey in value position refers to the only possible value of that type
ctx := context.WithValue(parent, ContextKey, 1)
Combining that with the idea that each variant of a union acts as if it were a named type leaves us in the same spot as in the previous comment where Direction.North can be used as a value, but with that being a consequence of the orthogonal rule rather than a special case just for union variants.
Of course this is assuming that there is no part of the language where it's ambiguous to the compiler whether an expression node is intended to be representing a value or a type. The proposal to let both unit and Direction.North be used interchangeably in both value and type position suggests that is true, but I've not verified that myself.
I don't think I've seen anyone discuss yet what I think is the largest cost of this proposal.
The problem is that allowing variants to be accessed using field syntax changes the meaning of this code:
x.Foo = "world"
x.Bar = 42
fmt.Printf("hello %s!\n", x.Foo)
In today's language, the reader can infer enough information about x's type from this code that the actual declaration of x is unnecessary to predict its behavior. If x can be a union type in this snippet, this is no longer true. This doesn't change the semantics of any existing code, because no existing code uses these types, but from the reader's perspective it requires nonlocal analysis to verify this. Go has long prioritized the experience of reading code over that of writing it, and this increases the burden on the reader significantly.
I think https://github.com/golang/go/issues/76920#issuecomment-5230016562 is a good direction, and I hope the original field syntax can eventually be removed from this proposal. I continue to think that splitting out a notion of "constructor" separate from type would fix a lot of problems, though my concrete proposal in https://github.com/golang/go/issues/76920#issuecomment-4501506632 doesn't work because it conflates assignability with convertibility, and it also doesn't take advantage of existing type switching and assertion syntax.
@ijschwabacher I can think of at least three things x could potentially be in your example in today's Go:
Foo and Bar.Foo and Bar.Foo and Bar.This proposal adds two additional possibilities:
Foo and Bar.Foo and Bar.In cases 1 and 3 this code would run to completion without panicking.
In cases 2 and 5 this code could panic on the first line if x were a nil pointer.
In case 4 this code would definitely panic on the third line.
Is your concern about the reader now having to consider the new situation that arises in case 4, whereas previously only the other two situations were possible?
Syntax study for T.V() and T.V{} syntax which I am informally referring to as "variant literal syntax." I just wanted to see all the forms in one place.
The T.V() form is both for types that do not have a composite literal syntax and faux conversions of the form T.V(value) that results in a new T of variant V with value.
(I also wanted to see how var looked instead of select but s/select/var/g if you prefer)
package p
type unexported struct{}
type X var {
U struct{}
N int
a, b image.Point
X unexported
}
func example() {
var unit struct{}
// in each group of statements,
// every line creates the same value
var v X
v.U = {}
v = X.U{}
v = X.U({})
v = X.U(unit)
v = X{U: {}}
v.N = 5
v = X.N(5)
v = X{ N: 5 }
v.a = {X: 1, Y: 2}
v = X.a{X: 1, Y: 2}
v.b = {}
v = X.b({})
v = X.b{}
v.X = {}
v = X.X{}
_, _ = v.(N) // not v.(X.N)
switch v.(var) {
case U:
case N, a:
case b:
case X:
}
}
import "p"
func example() {
var unit struct{}
var v p.X
v.U = {}
v = p.X.U{}
v = p.X.U(unit)
_, _ = v.(N) // not p.N
// cannot reference unexported variant
// v = p.X.a{}
// cannot create value of unexported type
// v = p.X.X{}
//
// but you can access it
_, _ := v.(X)
switch v.(var) {
case U:
case N:
case X:
default:
// a, b, or some future variant added to X
}
}
Using assignment to demonstrate isn't the best. v.V = x is better than v = T.V(x). It really shines in cases like f(T.V(x)) vs f(T{ V: x }).
I've become a fan of this syntax as it makes constructing values of T simple and readable and on par with functional languages.
I'm not sure regular composite literal syntax is worth it in addition to this new form.
I'm concerned that v.N = 5 can be confused with a regular struct with field N.
What about v.(N) = 5?
Separately, is there any usefulness in having 2 fields of the same type?
type Smth select {
A int
B int
}
And how would struct tags behave, if allowed?
type Smth select {
ConfigA string `env: "CONFIG_A"`
ConfigB string `env: "CONFIG_B"`
}
What about
v.(N) = 5?
That's already the syntax for a type assertion.
v.N = 5 is very much like setting a regular struct field. The only difference is that it may also unset a different field (if v is not currently on N)
I'm also wondering how confusing that would be if the type was embedded? But maybe that's a different conversation completely.
It's a good point that extracting a value from a sum is more like a type assertion than a field access. x = v.(N) is more verbose than x = v.N but conveys the expectation that the operation can fail.
I don't think "every variant is a type" works out very well. My approach in https://github.com/golang/go/issues/76920#issuecomment-5230016562 isn't entirely impractical, but it requires adding a new language concept (a name which is either a type or a value depending on context) and has some rough edges.
The semantics of this proposal include variants which act like values (Direction.North), and variants which act like types (IP.IPv4). Here's another attempt at the design which leans into that concept.
A union contains a set of variants. Each variant may have a name, a type, and a value. All three are optional, but not every permutation is valid (see below).
union {
_ = iota // no name, no type, unique value
N1 int // name N1, type int, no value
N2 = 2 // name N2, type int (inferred), value 2
N3 = iota // name N3, no type, unique value
N4 int32 = 4 // name N4, type int32, value 4
N5 = int32(5) // name N5, type int32, value 5
}
Within a union, variant names must be unique.
Values are specified with constant expressions and need not be unique.
A variant must have a type, a value, or both.
A named variant with no type must have the value iota, which denotes a unique value for the variant.
A value of union type stores a selected variant and a value of the variant's type. For variants with a value, the stored value is that value.
For example, we might specify Direction, Option, and IP as:
type Direction union {
North = "north"
South = "south"
East = "east"
West = "west"
}
type Option[T any] union {
None = iota
Some T
}
type IP union {
IPv4 [4]byte
IPv6 [16]byte
}
Operations on unions extend existing language constructs. Many of these rely on the following definition:
If U is a union and V is the name of a variant of U, U.V is a variant selector identifying that variant.
The zero value of a union selects the first variant with either that variant's value (if it has one) or its type's zero value (if it does not).
var d Direction // North, value "north"
var o Option[int] // None
var i IP // IPv4, value []byte{0, 0, 0, 0}
var u union { _ } // unnamed variant
Union types are comparable.
A comparison of two union type values that select the same variant panics if that variant's type is not comparable. (Note: I don't like this, but it's necessary to make if o == Option[[]byte].None {} work. Maybe that doesn't need to be made to work?)
For a variant with a value, the variant selector U.V is a variant expression yielding a value of type U with the variant V selected. Syntactically, a variant expression is similar to a method expression.
d := Direction.North
o := Option[int].None
For a variant with no value, U.V(x) is a variant conversion yielding a value of type U with the variant V selected and value x. x must be assignable to the type of the variant. Syntactically, a variant conversion is either a new form of conversion or a similar expression.
o := Option[int].Some(0)
ip4 := IP.IPv4([4]byte{127, 0, 0, 1})
For an expression x of union type containing a variant with a type and name V, x.(V) is a variant assertion asserting that x's selected variant is V. This behaves like a type assertion: run-time panic if the assertion does not hold unless used with an additional boolean return.
var o Option[int]
i, ok := o.(Some) // 0, false
_ = o.(None) // invalid: None does not have a type
i = o.(Some) // panics: Some is not the selected variant
A union switch compares variant selectors. A union switch may include a short variable declaration in the same fashion as a type switch. Cases of a union switch name variants. In a clause with a case listing variants with identical types, the variable has that type. Otherwise, the variable has the type of the union.
Union switches must list every variant of the union, or include a default case.
var d Direction
switch d.(union) {
case North:
case South:
case East:
case West:
}
var o Option[int]
switch v := o.(union) {
case None:
// v has type Option[int]
case Some:
// v has type int
}
switch v := d.(union) {
case North, South, East, West:
// v has type string
}
A value x of union type T, where every variant of T has an identical type U, can be converted to any type which U can be converted to.
string(Direction.North) // "north"
string(Option[string]) // error, Option[string]'s variants do not have identical types
Unions may not be constructed using composite literals.
o = Option[int].Some(42) // yes
o = Option[int].None // yes
o = Option[int]{Some: 42} // no
Values of union type may not be used as the subject of a selector expression.
var o Option[int]
o.Some = 42 // no
_ = o.Some // no
o = Option[int].Some(42) // yes
o = Some(42) // no (maybe this can be made to work?)
Do you envision unions subsuming named constants? e.g. const DirectionNorth = "north" → Direction.North?
type Direction union {
North = "north"
South = "south"
East = "east"
West = "west"
}
It's a good point that extracting a value from a sum is more like a type assertion than a field access. x = v.(N) is more verbose than x = v.N but conveys the expectation that the operation can fail.
I might be missing something obvious here, but isn't this a panic as well?
type a struct { b int }
func main() {
var foo *a
println(foo.b)
}
meaning x.y could fail today if x is a pointer. Is the argument that this could also fail for non-pointer enums/unions/sum?
@neild would the following be a compile time error
Direction.North("south")
?
Do you envision unions subsuming named constants?
A design goal is that you can use a union instead of a set of named constants defining an enumeration, in the case where you want a closed set of possibilities (The N/S/E/W direction case being a representative example.) The union wouldn't be appropriate in all cases; for example, we've got an http.MethodGet = "GET" constant but we accept arbitrary strings as HTTP method names so a union of method names would not work out well.
I might be missing something obvious here, but isn't this a panic as well?
(Where the panic is a nil pointer dereference.) Yes, but people really hate nil pointer panics.
Also, fetching a variant really is very much like a type assertion. x.(S) asserts that x contains a value of type S when x is an interface value, and that x contains variant S when x is a union.
Would
Direction.North("south")be a compile time error?
Yes. A variant selector x.V(y) requires that the variant have no value. You can't write Direction.North("north") either.
Embedding:
There are no issues with embedding a variant in a constraint. It means the same as any other non-interface type.
If you embed a variant in a struct, a field can shadow setting a variant but not asserting it (as s.F and s.(F) access different namespaces). It would be troublesome to have to remember which selectees of a struct use field syntax and which use assertion syntax. I see no issue with the methods of the variant type being proxied. Either embedding should be disallowed in structs or only proxy the methods.
Embedding a variant in a variant will be simpler to discuss with an example:
type Inner select {
A, B, C T
}
type Outer select {
Inner
A, D T
}
var set T
var val Outer
This works out largely how you would hope. Outer.A shadows Inner.A but it's no better or worse than any other shadowing caused by embedding.
This does mean if the current variant is Inner's A then there needs to be a double assert:
if i, ok := val.(Inner); ok {
_, _ = i.(A)
}
This could be made cleaner by allowing pathed asserts in such cases like val.(Inner.A) to query both simultaneously. Note that the zero value of Outer is the hard to reach Inner A so this case was hit in fewer than 10 LOC.
Say that Inner has a String method not shadowed by Outer which returns "A" for A and so on with default "<unknown>". This works as expected when Inner is not embedded or when it's embedded and one of its variants is the current variant. If val.D = set then val.String() returns "A" as Inner must be its zero value when not selected, which may not be immediately intuitive, but is forced by the definition.
Is there an advantage of typeless variants w/iota as opposed to doing:
type Option[T any] union {
None = true
Some T
}
?
A union contains a set of variants. Each variant may have a name, a type, and a value. All three are optional, but not every permutation is valid (see below).
From your examples, it seems that the name is not optional (but it might be _).
The zero value of a union selects the first variant with either that variant's value (if it has one) or its type's zero value (if it does not).
That means the zero value is not necessarily represented by all zero bits and e.g. creating a slice of a variant is O(n).
type E enum { V = 42 }
var y E // zero value is 42
var x = make([]E, 1<<20) // needs to initialize 1M values to 42
Note: I don't like this, but it's necessary to make
if o == Option[[]byte].None {}work. Maybe that doesn't need to be made to work?
ISTM that at least for Option, it would be much better if there was a func (Option[T]) Valid() bool or something anyways.
Overall, not a fan. It seems like a pretty complicated design requiring introducing a lot of new terminology and special cases. I think it tries to cram too many kind of different concepts and use cases into the same thing.
I assume the memory representation would be similar/the same as interfaces and union{I int; F float64} would be represented as one word for the variant pointer (like a type pointer in an interface) and one word for the value pointer. So
type E enum { V = 42 }
var y E // zero value is 42
var x = make([]E, 1<<20)
x would not need to initialize anything to 42, since a single 42 would be stored in a variant/type table for E.V somewhere.
It may or may not need to initialize the first word to variant zero. I think a sufficiently clever runtime could know that it's an E, so it's zero variant is E.V.
Hmm, it occurs to me that pointers are basically the same as Option, so the more interesting case is we could have
type Ref[T any] union{ Value T }
The ref would be functionally the same as a pointer, but it could never be nil…
memory representation
My hope is that sum types (whatever the semantics) would use a more efficient representation that avoids unnecessary allocation. For example, a sum with int and float64 variants needs one word for the variant selector and 64 bits for the value; no need for a value pointer.
type Ref[T any] union{ Value T }
This is effectively the same as just T; it's not a reference. If you pass it around, you pass around a T by value.
A design goal is that you can use a union instead of a set of named constants defining an enumeration, in the case where you want a closed set of possibilities (The N/S/E/W direction case being a representative example.)
Would this also include some way of listing the different possibilities (e.g. to fill a dropdown box)?
A design goal is that you can use a union instead of a set of named constants defining an enumeration, in the case where you want a closed set of possibilities (The N/S/E/W direction case being a representative example.)
Would this also include some way of listing the different possibilities (e.g. to fill a dropdown box)?
I’d expect that it would be possible via reflection. Just like it is for iterating over a struct fields today.
memory representation
My hope is that sum types (whatever the semantics) would use a more efficient representation that avoids unnecessary allocation. For example, a sum with int and float64 variants needs one word for the variant selector and 64 bits for the value; no need for a value pointer.
Sounds like the GC would need to be union-aware to discriminate between overlapping GC shapes.
Sounds like the GC would need to be union-aware to discriminate between overlapping GC shapes.
I think you could get pretty far storing unions as:
type _ struct {
variant int
ptrs [totalPtrs]unsafe.Pointer
other [nonPtrBytes]byte
}
Since you can't address the individual variants, thus you can order fields of union variants however you want.
@neild I see the value in letting variants have a constant value, but I don't know how I feel about mixing constant and variable variants, especially without an analogue elsewhere in the language. I suspect a lot of people would assume they were default values.
When all the variants are constants of the same type it's easy to emulate:
type Direction union {
North, South, West, East struct{}
}
func (d Direction) Value() string {
switch d.(union) {
case North:
return "north"
case South:
return "south"
case West:
return "west"
case East:
return "east"
default:
// needed to satisfy return rules?
return "<unknown>"
}
}
The iota quasitype (?) introduces a considerable collection of novel concepts and rules but does not seem to buy a lot over struct{} variants:
type Opt[T any] union {
None struct{}
Val T
}
func (o Opt[T]) And[S any](f func(T) S) Opt[S] {
switch v := o.(union) {
case None:
return {}
case Val:
return Opt.Val(f(v))
}
}
func (o Opt[T]) Or(x T) T {
if v, ok := o.(Val); ok {
return v
}
return x
}
If constants are included, iota should mean what it means in const blocks:
type Dir union {
North = iota
South
West
East
}
If not, variant expressions buy little over allowing T.V{} which introduces fewer concepts and in particular makes working with struct{} entries a bit nicer.
I'm going to restate the variant of this proposal from https://github.com/golang/go/issues/76920#issuecomment-5212257351, simplified and adjusted based on comments.
select typeWe introduce a new kind of type, a select. This is what other languages call a "union" or "sum" or "enum" or "variant" or "oneof" type: a type that can contain a single value that must be one of a list of types. We use the term select to avoid introducing a new keyword. (A new keyword would not be a backward compatible change, as it would break existing Go code that uses that keyword as the name of a type. While we could choose to break existing code, avoiding that is preferred if possible. select is already a keyword in Go, so using it in a new syntactic position poses no compatibility issues.) (Another possible name for this type is switch.)
Syntactically a select type is defined like a struct type:
SelectType = "select" "{" { FieldDecl ";" } "}" .
type IP select {
IPv4 [4]byte
IPv6 [16]byte
}
We will call the elements of a select variants. Each variant has a name and a type. A value of type select contains exactly one value, whose name and type is one the variants of the select. The zero type of a select is the name and zero value of the type of the first variant listed in the declaration. Code may assign to a select as though assigning to a field of a struct; the assignment will replace any existing value.
var v IP
v.IPv4 = [4]byte{127, 0, 0, 1}
v.IPv6 = [16]byte{} // v now holds a [16]byte value, not a [4]byte value
The value of a select always holds exactly one variant. Retrieving a value uses the syntax v.(name), such as v.(IPv4). This is a variant assertion. As with an interface type assertion, referring to a variant that is not the one stored in the select will panic, and a comma-ok statement may be used to fetch both the stored value and a bool indicating whether that was the stored value.
v.IPv4 = [4]byte{127, 0, 0, 1}
x1 := v.(IPv4) // succeeds, x1 gets value [4]byte{127, 0, 0, 1}
x2, ok := v.(IPv4) // x2 = [4]byte{127, 0, 0, 1}, ok = true
x3 := v.(IPv6) // panics
x4, ok := v.(IPv6) // x4 = [16]byte{}, ok = false
Values of type select may participate in a new construct, a select switch. Each case of the type switch must be the name of a variant.
switch x := v.(select) {
case IPv4:
// x is type [4]byte
case IPv6:
// x is type [16]byte
default:
// Can't happen in this case, but if it could x is type IP
}
The compiler will issue an error if there is no default case and there are some missing variant cases. This means that adding a new variant to an exported type is a breaking API change. This may be mitigated with a variant whose name is _. A select switch on such a type can never name all the variants, and must therefore always have a default case.
select values may be assigned to variables of the same type.
select values may have methods, and may be converted to interface types in the usual way.
select values may be compared to values of the same select type using == or !=, provided at least one variant has comparable type. Two values are equal if they have the same variant and if the variant values are equal. If the values being compared have the same variant, but the values of those variants are not comparable, the comparison will cause a run-time panic (for example, this could happen if the variant type is an interface type).
A select type may be used as a composite literal type. The composite literal must be keyed and must have at most one key/value pair.
While a variable of select type is addressable as usual, individual variants of a select are not addressable. This differs from individual fields of a struct.
Given a select type S with a variant named V, a program may refer to S.V; this is a value of the type S that holds the variant V with the zero value of that variant.
type Direction select {
North, South, East, West struct{}
}
d := Direction.North // d is type Direction, variant North, value struct{}{}
if d == Direction.South {
println("this case won't happen")
}
if d == Direction.North {
println("this case will happen")
}
switch d {
case Direction.North: // this case is chosen
case Direction.South: // this case is not chosen.
default: // neither is this one
}
Compilers are encouraged to pack select values as tightly as possible in memory. The exact size of a value of type select is not specified by the language, except to say that any given type must be at least as large as the largest variant type. The alignment must be the largest alignment of any variant type.
If the values bring compared have the same variant
small typo @ianlancetaylor
In between it being called select or switch, i think switch makes more sense. select currently is used for channels, with no direct correlation to a type selection. switch otoh already has its .(type) thingy, which correlates better to the situation here, imo.
Though, in this case:
switch x := v.(switch) {
case IPv4:
// x is type [4]byte
case IPv6:
// x is type [16]byte
default:
// Can't happen in this case, but if it could x is type IP
}
I'm not a fan of the double switch. Perhaps it could simply be switch x := v? Random thought, don't know how feasible that would be or if this would introduce some edge cases.
In my opinion, the problem with the "select" type proposal lies in the fact that it has already been used to refer to the process of selecting channels. It feels awkward as a separate type name.
@phenpessoa Thanks, fixed the typo.
@bigwhite I agree that select is awkward. And I know that @neild and others would prefer a new keyword, probably union. But a new keyword is expensive. There just isn't any good way to add one without significant ecosystem disturbance. So we have to weigh different costs: ecosystem disturbance or language awkwardness. If the consensus is that a new keyword is better, I'm fine with that.
Do we even need the U.V zero-value syntax? Go already has a convention for enum members: package level declarations.
package pkg
type Direction select {
North struct{}
South struct{}
East struct{}
West struct{}
}
var (
DirectionNorth = Direction{North: struct{}{}}
DirectionSouth = Direction{South: struct{}{}}
DirectionEast = Direction{East: struct{}{}}
DirectionWest = Direction{West: struct{}{}}
)
select seems like a perfectly acceptable keyword choice, AFAIAC.
Getting the functionality is preferable to me vs. stalling on a naming choice.
On Aug 17, 2026, at 12:10 AM, Ian Lance Taylor @.***> wrote:
ianlancetaylor
left a comment
(golang/go#76920)
<https://github.com/golang/go/issues/76920#issuecomment-5311672256>
@bigwhite <https://github.com/bigwhite> I agree that select is awkward. And I know that @neild <https://github.com/neild> and others would prefer a new keyword, probably union. But a new keyword is expensive. There just isn't any good way to add one without significant ecosystem disturbance. So we have to weigh different costs: ecosystem disturbance or language awkwardness. If the consensus is that a new keyword is better, I'm fine with that.—
Reply to this email directly, view it on GitHub <https://github.com/golang/go/issues/76920?email_source=notifications&email_token=AABFNROUAP5MIMUPTDNI3Y35KKAUFA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMZRGE3DOMRSGU3KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5311672256>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AABFNRLBPHOND24ISQEUMBD5KKAUFAVCNFSNUABEKJSXA33TNF2G64TZHMZDGMBZGY4TKOJ3JFZXG5LFHMZTONBVGAYDSMBVG2QXMAQ>.
You are receiving this because you were mentioned.
I'm ready to put forward a minimal concrete competing proposal (which I think is pretty close to The Book version of what it's trying to be), plus an optional extension that defines embedded variants, and finally a sketch of an extension that defines per-variant methods. I have a spec patch for the minimal proposal mostly drafted, but I got stuck writing clear prose to unify the type and variant switch concepts, which should not be a priority at this stage.
I am not enough of an expert on generics to be able to detect any nasty interactions this proposal might have with them, but there's enough expertise in this conversation already that I expect any issues will be noticed.
I've stuck with union as the keyword, because I really dislike reusing switch or select, and because I think it's possible to use go.mod shenanigans to introduce a new keyword without requiring code changes, but it's not essential.
EDITED: The original proposal created an ambiguity where it was no longer possible to distinguish literals from non-literals by syntactic means. The replacement relies on https://github.com/golang/go/issues/12854, which is accepted.
Above, @neild mentioned wanting to avoid having variants be neither values nor types. But they are neither values nor types. I propose that we just accept that, and let them be what they are, just as struct fields are (the part associated with the type, not the part associated with the value).
A union is a choice among a non-empty sequence of alternatives, called variants, each of which has a name and a type. The zero value of a union type is the zero value of its first variant. Variants may be tagged, similarly to struct fields; as with structs, tags contribute to type identity and assignability but not convertibility. The representation of a union value in memory is at least as large as its largest variant, but there are no guarantees about alignment or internal representation.
type Direction union {
North, South, East, West struct{}
}
type Option[T any] union {
None struct{}
Some T
}
type IP union {
IPv4 [4]byte
IPv6 [16]byte
}
```
A value of a union type is self-contained, so it is invalid for a union of type `T` to contain a variant of type `T`, or of a type containing `T` as a component, directly or indirectly, if those containing types are only array, struct or union types.
## Union literals
A union literal constructs a new union value of a chosen variant from a value assignable to that variant's type. The literal is formed by writing the variant's name, qualified by the union type's name, with the value in braces. If the value is itself a fully-brace-enclosed (i.e. non-union) [type-inferred](https://github.com/golang/go/issues/12854) composite literal, one set of braces may be omitted unless this makes the type inference ambiguous. A type-inferred union literal omits the type and the joining ".", but not the variant name.
dir := Direction.South{}
o1 := Option.Some{Option[int].None{}}
o2 := Option.Some{1}
o1 == Some{None{}}
## Variant assertions
Type and variant assertions are unified into a syntactic construct called "narrowing assertions". A narrowing assertion reinterprets the value of an interface or union type as a type corresponding to one of the specific alternatives available to that type. For a type assertion, the alternatives are types compatible with a given interface type (i.e. non-interface types implementing the interface, plus interface types not conflicting with the interface), each corresponding to itself; for a variant assertion, the alternatives are the variants of a given union type, each corresponding to its associated type.
ip := IP.IPv4{127, 0, 0, 1}
v4 := ip.(IPv4) // v4 == [...]byte{127, 0, 0, 1}
v6, ok := ip.(IPv6) // v6 == [16]byte{}; ok == false
> **NOTE:** Without explicit syntactic support, nested unions are awkward to work with. We should consider permitting this:
>
> ```
> o := Option[Option[int]].Some{None{}}
> _, ok := o.(Some{None}) // ok == true
> i, ok = o.(Some{Some}) // i == 0; ok == false
> ```
>
> An alternative is hacking things so that this doesn't panic:
>
> ```
> o := Option[Option[int]].None{}
> i, ok := o.(Some).(Some) // needs help to produce i == 0; ok == false instead of a panic
> ```
> **NOTE:** As with a type assertion, the value of `v.(A)` is a shallow copy of the value stored in the variant. There's no syntactic construct that looks like a reference into the variant, so there's no need to explicitly explain that the value is not addressable.
## Variant switches
Type and variant switches are unified into a syntactic construct called "narrowing switches". A narrowing switch compares alternatives rather than full values, yielding a value of the associated type. It is marked by a special switch expression that has the form of a narrowing assertion with a keyword rather than an actual alternative. For a variant switch, the keyword is `union`.
o := Option.Some(Option[int].None{})
switch i := o.(union) {
case None:
print("None{}")
case Some:
switch j := i.(union) {
case None:
print("Some{None{}}")
case Some:
print("Some{Some{", j, "}}")
}
}
> **NOTE:** As before, I am unsure whether to allow `case Some{None}:` and so on.
---
# Extension: Embedded variants
The previous proposal is sufficient, but I think this extension is also worth pursuing: here, we allow embedding variants. If a union type `U` has an embedded variant `T` (and `T` is not embedded more than once using aliases), a value of type `T` (not: a value assignable to type `T`) is assignable to type `U`. If an embedded variant is itself a union type, its variants are promoted to be variants of the embedding type using the same depth-based logic as for structs.
type stringish union {
string
fmt.Stringer
}
func (s stringish) String() {
switch ss := s.(union) {
case string:
return ss
case Stringer:
return ss.String()
}
}
var (
s stringish = "hello" // s == string{"hello"}
ss stringish = s // ss == string{"hello"}
si stringish = fmt.Stringer(s) // si == Stringer{string{"hello"}}
)
In this extension, I think we very much want the `o.(Some{None})` syntax, as it makes naming shadowed embedded variants easier.
---
# Extension: Variant methods
This extension is more speculative. Here, we permit defining methods with a union variant as the receiver type. Only value methods are permitted; pointer methods are forbidden. The variant receiver is a syntactic construct only: inside the method body, the receiver has the variant's associated type.
func (Direction.North) String() { return "north" }
func (Direction.South) String() { return "south" }
func (Direction.East) String() { return "east" }
func (Direction.West) String() { return "west" }
var (
dir Direction
_ fmt.Stringer = dir
)
func ( Option[T].None) AndThen( func(T) U) Option[U] { return None{} }
func (o Option[T].Some) AndThen(f func(T) U) Option[U] { return Some{f(o)} }
Methods of embedded variant types are promoted to their respective variants (with the restriction that because there are no variant pointer methods, pointer methods on by-value-embedded types are not promoted at all), and it is permitted to override a promoted method with an explicitly defined variant method. A method that is present on all variants defines a method on the union type; it is an error if an explicitly-defined variant method does not participate in defining a union method in this way.
type SillyIO union {
io.ReadCloser
io.WriteCloser
}
var (
i SillyIO
_ io.Closer = i
)
func (s stringish.string) String() {
return s
}
var (
s stringish
_ fmt.Stringer = s
)
```
NOTE: I'm undecided on how methods on the union type should interact with methods on the variants. We could forbid methods on the union type entirely, or we could forbid them from overlapping with methods on the variants, or we could use them as fallbacks:
func (Direction.North) IsNorth() bool { return true } func (Direction) IsNorth() bool { return false }NOTE: We could allow pointer methods if we performed some sort of analysis to forbid the pointers from escaping, but that would only permit within-variant mutation of the receiver, and also... yikes. Thinking about how to implement this with the representation in https://github.com/golang/go/issues/76920#issuecomment-5258111316 certainly explains why @neild is considering permitting variants to be their own types, but I think trying to make this work without rust-style lifetimes is more trouble than it's worth.
I like this proposal in the abstract, but Go is it is today has a lot of code like
type Direction int8
//go:generate stringer -type Direction
const (
North Direction = iota
East
South
West
)
I would like it if there were some way to make these two styles of enums interoperable or upgradable. I don't have a good idea for how to do it though.
Just a note about adding a new keyword: it's important to consider gofmt. gofmt currently is not language version aware. gofmt can be run on a single file, or even just a partial file. It would be very desirable for gofmt to support the new keyword while still doing the correct thing for existing files that happen to use the new keyword as an ordinary identifier.
As others already pointed out, select is a confusing name because the previous use of that keyword has been exclusively related to channels and yet this new proposal has nothing to do with channels,
switch does seem better on the surface and I was in favor of that at first, but then I realized it creates an annoying terminology hazard:
switch v := x.(type) { /* ... */ }type Foo switch { /* ... */ }That seems too confusing even before introducing a "variant switch" with the syntax switch v := x.(switch), which is neither a type switch nor a switch type but nonetheless contains two mentions of the keyword switch[^1]. 🤯
Both of these keywords seem confusing for different reasons, but if it has to be one of them then I think select is better if only because the language features it has naming collision with are less likely to appear close by one another in practical programs.
None of the other available keywords already in the language seem defensible, so if adding a new keyword is undesirable then I think select is unfortunately the best available choice.
[^1]: I suppose we could lean right into it and call switch v.(switch) a "switch switch", just as "type switch" is the same shape but with "type" in the parens. 🙃
The original proposal seemed to call for the union keyword to be valid in only a small number of contexts:
type Name unionswitch v := v.(union)Personally, I'd rather have the complication of the union being recognized as a pseudo-keyword only in those two contexts than to overload one of the keywords already present in the language. That complexity lives in any parser for the Go language, including both the parser inside a compiler/tools and the grammars used for syntax highlighting in text editors etc.
There is admittedly an annoying inconsistency where v.(union) in the header of a variant switch would use it as a keyword while v.(union) as a type assertion would interpret union as an identifier referring to a type, and so a naive implementation would return a confusing error message for the type-assertion case.
I tried to look through the existing discussion since I thought I remembered this already being discussed, but I didn't find anything, so: is there a technical blocker to having parsers treat union as a pseudo-keyword only in those two positions, and as an identifier in every other position? 🤔
Edit: Answer to this in https://github.com/golang/go/issues/76920#issuecomment-5318947184 below.
https://github.com/golang/go/issues/76920#issuecomment-5318417676:
I just noticed that https://github.com/golang/go/issues/12854 is accepted, so you will be able to write this:
type Direction union {
North, East, South, West struct{}
}
var North, East, South, West Direction = North{}, East{}, South{}, West{}
and carry on from there.
@apparentlymart Regarding a pseudo-keyword, it's unfortunately not as simple as that. Unless we introduce some weird restrictions, types, including union types ,may appear in several other contexts, such as type conversions and type assertions and variable declarations.
fmt.Println(union{ V int }(union{ V int }{1}))
a := b.(union { V int })
var u union { V int }
switch a.(union) { ... } // new union switch
switch a.(union { V int }) { ... } // switching on a type assertion
I’m in favor of the select keyword. I’m not enthusiastic about adding a new keyword even if there was one that I liked better, given the tooling ecosystem cost. I honestly don’t think union conveys the meaning better than select. If anything, oneof is the better name.
I have updated the proposal to resolve a syntactic ambiguity.
Starting from @ianlancetaylor's latest version, I wonder if the core can be a little smaller by leaving out field assignment and T.V for now.
The basic model would be:
type Result[T any] select {
Value T
Err error
}
// construct
r := Result[int]{Value: 42}
// extract
v, ok := r.(Value)
// match
switch v := r.(select) {
case Value:
// v is T
case Err:
// v is error
}
I like the latest proposal's assertion-style extraction to the payload type and exhaustive select switches. The part I'd simplify is construction.
I'd start with keyed composite literals and leave out field assignment such as r.Value = 42. Assigning a variant isn't quite ordinary field assignment: selecting Value also deselects Err. Since keyed literals already let us construct every variant, I don't think we need to give field assignment that additional meaning as part of the core feature.
A shorter constructor syntax may well be worthwhile, especially in nested expressions, but that can be considered separately.
For the same reason, I'd defer the proposed T.V rule. We don't need it to support enum-like types:
type Direction select {
North struct{}
East struct{}
South struct{}
West struct{}
}
var (
North = Direction{North: struct{}{}}
East = Direction{East: struct{}{}}
South = Direction{South: struct{}{}}
West = Direction{West: struct{}{}}
)
d := North
if d == West {
// ...
}
switch d.(select) {
case North:
case East:
case South:
case West:
}
Direction.North would certainly be more convenient. The point isn't that the longer form is better, only that zero-sized variants plus ordinary values are enough to support this use case. T.V can then be considered separately as an ergonomic feature rather than something the basic sum-type semantics depend on.
So, taking the latest proposal as the baseline, the smaller question I'd like to resolve first is: do the semantics of the basic sum type require anything beyond keyed construction, variant assertions to the payload type, and exhaustive select switches?
If not, field assignment, T.V, and other conveniences can be evaluated independently.
I agree that
d.IPv4 = x
isn't really that much better than
d = IP{IPv4: x}
This is especially true if type inferred composite literals let us do just d = {IPv4: x}
It's a little verbose for the direction case, but only because you have to write struct{}{}. Maybe a name for that would help.
I agree that
d.IPv4 = xisn't really that much better than
d = IP{IPv4: x}
similar case is with normal structs:
type foo struct {
x, y int
}
var a foo
clobberY(&a)
a.x = 3 // a.y might not be zero val
clobberY(&a)
a = foo{x: 3} // a.y is the zero val.
We often use the composite literal syntax to make other fields zero-val, similar analogy transfers to unions (i think).
@mateusz834
Right, I think we agree with each other. d.IPv4 = x is unnecessary and we can leave it out. d = {IPv4: x} is enough (my argument) and clearer (your argument).
Or am I misunderstanding?
A struct field is effectively a function from the struct type to the field type. This is why we write value.field using a syntax similar to value.method(...) (which is sugar for type.method(value, ...) when we get the field out of the struct, and a different syntax for putting it in. In contrast, a union variant is effectively a function from the variant type to the union type, and we should write the variant using a syntax that looks like the function name in a call when putting the variant in, and use a different syntax for getting it out. My initial (pre-edit) proposal did exactly this, though I updated it to use braces instead of parentheses so that union literals could be distinguished from function calls syntactically.
I do agree that moving away from parenthesis is a good idea as we wouldn't want a conflict with method on the sum type. I don't know if braces are a good way to to do it, but at least I think it wouldn't cause a conflict
My thinking is that braces as syntax say "this is a literal". When you are reading code and you see parentheses, they might be doing real work. You need some sort of context information to tell you whether this is a function call or a conversion or what, and if it's a function call you need to know its body. When you see braces, in contrast, you know just from what you're reading that no real computation is being done.
Sorry for the drive-by comment since I'm not a real Go user, but sum types are an area I've spent time exploring in other contexts.
I don't think "every variant is a type" works out very well.
I agree. I believe there is another usability issue I haven't seen mentioned yet here. Consider:
currentDirection := Direction.North
if turnaround {
currentDirection = Direction.South
}
This code seems innocuous. But if each variant defines a distinct type and the expressions that create a union use the variant as their type, then here currentDirection's inferred type is the too-precise type Direction.North and not Direction. That means the subsequent assignment in the if is an error. A user can work around that by explicitly typing the variable:
var currentDirection Direction = Direction.North
if turnaround {
currentDirection = Direction.South
}
But in practice, I think that would feel egregiously annoying. Especially in unions like Direction where there's no practical use for each variant to have a separate type (since the types don't carry any interesting state), it would be annoying to have to manually annotate the more general union type all the time.
If every variant expression has the same type (the type of the union), then freely assigning between them and using type inference on local variables with them gets easier.
You could try to have it both ways by having each variant define a type but have the variant creation expressions still have the type of the overall union. I suspect that would just feel strange and confusing for the expression Direction.North to not have type Direction.North when Direction.North is a type.
If a user does want a variant to have a type, they can always make the variant's field be a struct of some type they declare and name separately (which is what you would do in Rust, SML, etc.).
I notice in https://github.com/dart-lang/language/issues/546 that you wrote about ADTs:
I think patterns are the most important core to all of these features. You obviously can't do pattern matching, destructuring, and exhaustiveness checking at all without them. You technically can do tuples, sum types, and data classes, but I think they are significantly hampered without them.
Here we are obviously going against that advice. Do you have any specific recommendations on how not to box in any future pattern-matching syntax that might be added to go? I have skimmed https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/goals-and-constraints.md but not digested it yet.
Do you have any specific recommendations on how not to box in any future pattern-matching syntax that might be added to go?
Given that the Go ethos is very conservative about adding features and tends to lean towards preferring simple imperative code, I'd honestly be pretty surprised to see pattern matching and destructuring appear in the language.
The fact that Go already has multiple returns and multiple assignment would make it somewhat harder since that sits on some of the syntax that is often used for tuple destructuring (though you could probably route around it by requiring parentheses for tuples).
Go has very similar syntax for map, array, and slice literals. Pattern syntax usually mirrors the corresponding syntax for creating values, so that would make it harder to have distinct map and array/slice patterns. (Also, deciding whether to have patterns for both slices and arrays or unify them somehow is another hard decision.)
Hmm, in short, whoever is brave enough to try put patterns in Go has their work cut out for them. :)
...So of course I just tried to come up with a pattern matching syntax, and of course the first thing I produced was a pointer into a variant, which is exactly the thing I wanted to structure the syntax to forbid.
Hi All - thanks for keeping this on the radar. I'm not looking under the hood of "how" this gets implemented BUT want to just re-emphasize how important the feature of sum types and pattern matching are to quality of life writing & reading the code. For whoever that makes the decisions is listening - please prioritize this feature!