Open ianlancetaylor opened 1 year ago
@merykitty No, in your example, Addable itself should not be able to instantiate Add. Addable does not implement itself (only int and float32 do).
@Merovius So what you mean is that an Addable
does not satisfy itself during generics parameter resolution but it will do during runtime assignments? That seems a little confusing to me.
@merykitty Maybe. It's already a situation we will be in with Go 1.20 and comparable
. So there is precedent for these two things to be different. I also don't think it's an entirely natural idea for these to mean different things in different contexts - that is, an Addable
variable is "an opaque box that can hold any of these types" while an Addable
constraint is "the type argument must be any of these types".
So, yes, I think there is a certain amount of possible confusion here. But I'm not sure how confusing it'll be, how often it will be a problem and I'm not sure it's avoidable. Surprisingly, there are things which are confusing if you think about them, but if you don't, you just never notice. For example, I doubt most Go programmers could really explain why they can't use a bytes.Buffer
as an io.Reader
, even though they can call r.Read
on it - but in practice, they manage to use it just fine.
@merykitty you can see it as Addable not satisfying itself in both cases (the constraint being that something should be either int and float32)
It should implement itself however (the Addable type implements the same constraint (i.e. enforce the same contract as itself)) . Note that interface{int} also implements Addable as it merely enforces the contract more strictly: not only arguments are int or string, but we know for a fact that they have to be int.
Modulo nil, which I'm optimistic (or at least hopeful) can be solved.
This is similar to a ReaderWriter interface implementing the Reader interface. (subtyping)
@merykitty FWIW as an analogy: It also doesn't seem like many people are confused that an io.Reader
variable can't contain an io.Reader
- that is, the dynamic type of an interface is never an interface itself. It's essentially the same situation, it's the same confusion, yet in actual practice no one really wonders why that is.
If we had:
type Vehicle interface {
Car | Bicycle
}
type Mover interface {
Move()
}
would we say that Vehicle
satisfies Mover
if each element of Vehicle
(Car.Move,
and Bicycle.Move
) do as well?
@AndrewHarrisSPU As I interpret the proposal, you'd need to have a Move()
function on Vehicle
so that Vehicle
types implement Mover.
type Vehicle interface {
Car | Bicycle
Move()
}
type Mover interface {
Move()
}
Or you could do:
type Vehicle interface {
Car | Bicycle
Mover
}
type Mover interface {
Move()
}
@AndrewHarrisSPU:
If we had:
type Vehicle interface { Car | Bicycle } type Mover interface { Move() }
would we say that
Vehicle
satisfiesMover
if each element ofVehicle
(Car.Move,
andBicycle.Move
) do as well?
Worth noting that that does not work with constraints currently, either: https://go.dev/play/p/TLkZkYzOcdO
I think it makes sense for it not to work. It would be quite confusing and annoying to have to go searching through every type listed and figuring out the intersection of their available methods to see what you could do with it. Instead, just rely on the principle of defining interfaces where they're used and add the expected methods to the interface manually, which should then work.
@DeedleFake
Worth noting that that does not work with constraints currently, either: https://go.dev/play/p/TLkZkYzOcdO
I think it makes sense for it not to work. It would be quite confusing and annoying to have to go searching through every type listed and figuring out the intersection of their available methods to see what you could do with it.
If we can define a truly disjoint, finite, non-nil-able (or at least nil is an explicit element) type set, we can't include interfaces, but do we need interfaces to reason about the behaviors that are defined on that type set? I'm thinking (maybe naively?) that a compiler can tractably compute various sets-of-method-sets from the type set here. In practice I think a compiler could emit some precise and useful information ("error: Vehicle union doesn't implement Brake(): jetpack doesn't implement Brake()").
Going off-track a bit, I think there's also cases where defining a method on an element in a union itself could be interesting - I could call SetAlpha()
on the union of rgb
and rgba
and still maintain a valid union, but not on an rbg
value in isolation. In this case the union could satisfy SetAlpha()
, but not if we required SetAlpha()
to be defined on rgb
.
I believe the need to explicitly list methods in interfaces containing union elements is an implementation restriction by the current Go compiler and should be lifted sooner or later. I don't see a good reason why it can't (though sometimes these things are surprisingly subtle - there are other implementation restrictions which I don't think can be lifted, or am at least skeptical about). Though I don't understand what "nil-able" has to do with it, you can call methods on nil
values just fine.
Also, I agree with the criticism that it's a downside not to be able to define methods on union types, if they are defined like this proposal. Though a lot of the boiler plate can probably be reduced by struct embedding. As for the SetAlpha
example, an alternative would be to have the method be RGBA() rgba
, which could be implemented on both types and the usage would then be x = x.RGBA()
, instead of x.SetAlpha()
, which doesn't seem that bad.
Though I don't understand what "nil-able" has to do with it, you can call methods on
nil
values just fine.
With a closed, finite type set, do we have to recycle nil
as a catch-all? I think we could disallow declaring an instance of a sum type without declaring a variant, and ask implementors to explicitly provide empty/zero/undef variants - at least, I really enjoy this about sum types when I've used them in other languages. If a sum type exhibits a field that is unsafely nil
, maybe that could be regarded as programming error that justifies a resulting panic
just like it would otherwise.
I'm not sure it'd be insurmountable to do things more like the proposal suggests, and recapitulate the nuances of nil
s and interfaces, but it makes me nervous ... it looks manageable in small type switches but I think it could get nastier in practice - hard to reason about disjointness.
Currently the language seems to rely on every type having some meaning for the value that is represented as all zero bytes in memory. For example:
type Example interface {
int | string
}
m := make(map[string]Example)
v, ok := m["foo"]
Under the current proposal I would expect v
to be nil
because that is the zero value of Example
. If a nil
Example
were forbidden then it isn't clear what v
ought to be here.
Personally, I feel okay with accepting nil
interface values as an established part of the "texture" of Go and having these "sealed" interface types inherit that assumption, rather than introducing the one situation where there isn't a zero value and dealing with the effects on all other parts of the language that gave been defined on the assumption of zero values, although I do agree that it'll mean that patterns from other languages with different type systems won't translate over exactly.
Each time I revisit this I find myself thinking that this feature perhaps deserves a more specific name than "sum types" to help make it clearer that this is just an application of the theoretical idea of sum types to some specific situations, and not something that is intended to cover all possible use-cases for sum types. I still quite like "sealed interfaces" because it seems more clearly a special kind of interface and so inherits most of what we're already accustomed to with interfaces (including nils and method sets) and focuses only on constraining the full set of implementers at the declaration site.
Although I don't think it's a deal breaker, I think it is notable that declaring all of the implementers inside the interface
block means that the package which defines the interface must import any packages which export types that will be included in the set.
This means that the package that exports a type set member would be unable to import the package containing the interface and so could not name the interface type to use it in its own code without creating an import cycle.
Most of the use-cases we discussed above aren't impacted by that problem so I think this proposal is still useful despite it, but I do think it's interesting to think about given that it seems to invert the usual way that interface implementation works, where it's the package that defines the implementer that is responsible for (implicitly) adding it to the type set of the interface.
Although I don't think it's a deal breaker, I think it is notable that declaring all of the implementers inside the interface block means that the package which defines the interface must import any packages which export types that will be included in the set.
That's actually one of the points of the proposal. The idea is to create something analogous to C's union
s or Rust's enum
s. For example, consider the case of scanning a stream of tokens. It makes sense to have predefined types for the various token types, such as
type Token interface {
Number | String | Operator
}
type Number struct { /* ... */ }
type String struct { /* ... */ }
type Operator struct { /* ... */ }
// Not a great API, but it demonstrates the idea.
func Parse(r io.Reader) ([]Token, error) {
// ...
}
There are a surprising number of situations where a value can be limited to one of a handful of possibilities that are all known in advance. This proposal is designed to improve the ergonomics around those situations. The current way that something like the above is usually handled is to define the token type as type Token any
, but this is error prone because the types are less discoverable and it loses potential features that are only possible if the compiler is told what all the possibilities are, such as the potential ability to remove boxing mentioned in the proposal itself, linter enforcement of exhaustive type switches, and so on.
@Merovius
I believe the need to explicitly list methods in interfaces containing union elements is an implementation restriction by the current Go compiler and should be lifted sooner or later. I don't see a good reason why it can't (though sometimes these things are surprisingly subtle - there are other implementation restrictions which I don't think can be lifted, or am at least skeptical about).
Regardless of whether it can be lifted, I think it should not be.
If you have A | B | C
and those types happen to all have an M
method, you can never add a type D
without a method M
to the union—even if M
is irrelevant to the purpose and use of the union—because that would remove M
from the union's method set.
To be more concrete, I imagine this would happen quite (most?) often when M
= String() string
.
I think whether
switch
is exhaustive is an entirely independent proposal to sum types. It can be proposed independent of any type changes, and it doesn't need to be attached to this proposal.
It can't be added later, because it would break by then existing programs. A decision would have to be made together with this proposal.
@jimmyfrasche You already can not add types to an exported union, without breaking compatibility, regardless of what methods the types involved have. I don't think allowing to call methods would change anything.
FWIW "adding members to a union" is similar in effect on their type set to "removing a method from an interface" and "removing a member from a union" is similar to "adding a method to an interface". So, unless you are the only user of a union, you really can't do anything about it.
In fact, that's kind of why people want unions. They want a closed set of types. If you could change that set, it would no longer be closed.
In fact, that's kind of why people want unions. They want a closed set of types. If you could change that set, it would no longer be closed.
It's also why I don't like it. Interfaces are a tool to grant freedom, to empower users to provide their own types by implementing them, even if the original author of a function didn't think of them. They mean to open the world for extension, not to close it off.
Yupp. FWIW in #19412 I brought up the inability to ever modify a union as an argument against their inclusion into a language that is - at least in part - deliberately designed to allow for gradual evolution of APIs a bunch of times. I'm not sure I still totally buy it, as most type system features kind of work that way in one way or another.
But it is something that might be a bit easier with a first-class union/sum type, as you wouldn't run into this aspect of constraints already having variance. For example, if you required a type-switch over a union to always have a default
case and made it impossible to assign them to other unions (even if they are subsets of each other), I think you could then add new cases to them backwards compatibly. So, at least in part, it's a point against this specific implementation (overloading union elements for constraints).
@Merovius the situation is kind of different in that even if you are doing a v(N+1) you can't add it unless you can add the method (not an option if you want to add a primitive type) or be sure no one relied on the existence of the accidental method (and it's not even obvious that you need to look for this since it sneaks in implicitly). Unduly brittle for little gain when it'd make much more sense to be explicit.
I don't think I understand. Why wouldn't you be able to do a v(N+1)? And why is "being sure no one relied on the existence of the accidental method" any harder than "being sure no one ever used your interface as a constraint and then called a differently constrained function with it"?
(In any case, this is probably off-topic; this proposal is not about allowing to call methods not explicitly mentioned in an interface with unions)
Returning to the go/ast.Node example for a second, an advantage of a union type there would have been to consolidate the definition to one location. type Node interface { *AssignStmt | *BadDecl | ... }
. Currently what implements an ast.Node is spread out a bit over a pretty big file (and is kept readable via discipline in how things are written). Only needing to consult one place in the code would have helped me read/use this and similar libraries in the past. This can be addressed by tooling so it is an overwhelming advantage. But overall I think this proposal would help with the readability of some packages.
I suspect we will not be putting 56 cases (# of ast.Node impls in ast) separated by '|' on the same line. So I would anticipate there will be a lot of trailing '|' for larger cases.
type Node interface {
*AssignStmt |
// more *Stmts
*ArrayType |
// more *Types
...
}
Still readable enough IMO, but worth taking into account.
(Not suggesting Node change from its current meaning of range of token.Pos. The token range definition has other existing uses and is a good example of where not to use a closed type set.)
Why is this spread out over a big file and not listed in a comment?
@timothy-king I think it would rather be written as
type Stmt interface {
*AssignStmt | *BadStmt | *BlockStmt | … | *TypeSwtichStmt
}
type Expr interface {
*BadExpr | *BinaryExpr | … | *UnaryExpr
}
type Decl interface {
*BadDecl | *FuncDecl | … | *GenDecl
}
type Node interface {
Decl | Expr | … | Stmt
}
There's still relatively long unions there, but it gets more manageable (and it might even be possible to break them up further).
@Merovius I suspect Stmt
and Decl
would not be exported in this case. But your point that these could be broken up into a union of union types is well taken.
@apparentlymart
If a nil example were forbidden then it isn't clear what v ought to be here.
Definitely this would require something heavy-handed, I have strong opinions here based only on speculation, but for the sake of speculation - there are places in Go (like having to make maps and chans) where the builtins get special cases, and I’d be interested in going to these lengths to eliminate a ubiquitous ‘nil’. There might be better ideas, disabling the walrus operator for sum types would be brutal and special but seems like one option.
Even if it’s in the machinery that somehow, somewhere a truly invalid instance might panic, I really think the only reasonable response to a ‘nil’ variant of a sum type is panic. Otherwise it’s very tempting to treat such an instance as a zero value of some other included variant, or a predicate to produce a valid value. Then, as a reader of that code, or a writer of code employing an unfamiliar sum type, I simply do not have the ability to immediately observe that ‘nil’ is a properly disjoint case.
We've gone down the path of some types not having a zero value several times in the past, and it's never worked. Let's not go down that path again. Let's just assume that in Go types must have a zero value. Thanks.
And since the proposal here is for a particular kind of interface type, and since the zero value for all interface types is nil
, that is what this proposal says also. We can certainly discuss a sum type that has a different zero value (there is a lot of discussion over at #19412). But it would be very strange to say that for some interface types the zero value is nil
and for some other interface types the zero value is something else. That is a level of complexity that I don't think we are going to add to the language.
@timothy-king Note that both Stmt
and Decl
are already existing and exported interface types in the ast
package.
@apparentlymart @AndrewHarrisSPU One idea would be to simply disallow non-nilable unions as channel or map Types (and elsewhere)
type Example interface{
int | string
}
type NilableExample interface{
int | string | nil
}
m:=make(map[string]Example) //compilation error: nil(type?) is not in the type set of Example
m:=make(map[string]NilableExample) // OK
e, ok:= m["something"]
//...
v, ok:=e.(Example) // regular type assertion to check that it's a legit Example and not nil.
Of course, the zero value of Example
would still be nil. That doesn't change.
But such a value would only be created by variable declaration and not assignable where a non-nilable Example is expected.
So would come down to having to be explicit about nil.
One idea would be to simply disallow non-nilable unions as channel or map Types (and elsewhere)
Or slice types. Or fields. Or interface values.
The language doesn't like types without zero values. That can't really be helped.
Not necessarily a big issue for slice types either, or fields. One could still define the field case with the explicitly nilable supertype.
Some operations have to be disabled or modified otherwise, for slices of non-zeroable union values, that's true:
clear
differently or disallow it(edit: I don't even think that it's important to be able to do that.
If a variable of type T is not zeroable (doesn't mean T doesn't have a zero value btw, just that it cannot be assigned the zero value for that type although var v T is the zero) , one can simply define a slice of {T | nil} which is explicit.
Because the current semantics of slices demand that each indexed slot can be empty or emptied. Essentially, there are a few things that require optionality/ability to assign zero (to denote the lack of value) but that is easily built)
It's merely switching from nilable by default to nilable by construction which should be safer.
I don't think it would be much of a problem a priori. That's workable afaict. It's more an issue of proper value initialization, i.e. assignability.
For interfaces what do you have in mind as an issue?
Edit: I was randomly browsing and came across a similar treatment in Dart https://dart.dev/null-safety/understanding-null-safety So it is possible. I still believe this would be more workable for Go unions since it reuses traditional mechanisms such as type assertions. If subtyping was made more prevalent one day, that could be even further improved but it's not a necessity.
FWIW the idea of non-nilable interfaces has exactly the sample problems as the periodically happening discussion of non-nilable pointers. It's the same problem. It's not going to happen.
I'm not sure of what you mean. I'm striclty talking about unions.
Basic interfaces would remain the same. And I don't know what a non nilable pointer is.
Edit: I was randomly browsing and came across a similar treatment in Dart https://dart.dev/null-safety/understanding-null-safety. So it is possible.
Not to point out the obvious, but Dart is not Go. This is about how other aspects of the design of Go assume that every type has a zero value. Obviously, a language that is not designed under this assumption doesn't have this problem and there are many languages without nil
.
Well I'm well aware obviously... , if you browse through it, there are a few sections that might be of interest such as the fact that they had to deal with initialization of variables. (to implement null safety after the fact!)
So appears that some issues were still shared and they've found a way.
Just saying that it's possible, not that it will be done but before we shoot the idea down for unions, might be interesting to explore it.
I used a map element as an example earlier but note that even a type assertion -- an operation specifically for interfaces, and so would be weird to ban here -- relies on zero value for the non-matching case:
v, ok := interfaceVal.(Type)
Although the type in a type assertion can be a non-interface type, an interface type is also valid in that position and is a common pattern for detecting if the dynamic type in the interface value also implements a more specific interface.
In that situation if the test fails then v
is the zero value of the given interface type, which is always nil
in today's Go.
This is just one more of many places where Go assumes there is a zero value of every type. I don't think it's feasible to simply ban a particular type from any situation where a zero value is required, because that assumption is all over the language.
To be accurate, the zero value should still exist and would still be nil.
The issue is rather definite assignment analysis. It is sensitive to branching.
As long as this kind of analysis can be made fast, in a modular fashion, and without false negatives/positives, it should probably be fine.
I think Go might be one language for which it might be possible. (there are others, historically). I don't know if the ssa backend might not be of help here.
That's something to study.
@atdiar As soon as it is possible to create a zero value, it is literally impossible to guarantee that it's not getting assigned. That is, if I can create a nil
T
, it is impossible for a compiler to prove that any given T
is not nil
. Moreover, you've been told multiple times, by multiple people now, that this is infeasible and not going to happen. At this point, saying that there is "something to study" is pretty frustrating. There just is not. Take a "no".
@atdiar
As long as this kind of analysis can be made fast, in a modular fashion, and without false negatives/positives, it should probably be fine.
Take this function
func AddNElems[T any, S ~[]T](slc S, n int) S {
return append(slc, make([]T, n)...)
}
Can we prove that this function take only slices of non-nilable elements?
@DmitriyMV The slice type constructor implicitly requires that T is nilable/zeroable (because a slice can be cleared) . I don't see why such a check would be infeasible.
@Merovius What makes you think that I have to be compelled to agree with you? You don't even understand what I am saying. I'm not even claiming that a variable shouldn't be assigned. I'm saying that a variable shouldn't be assigned the zero value in certain cases. Before claiming that something is impossible, one might want to study. This is not the first time that you make such claims and exhibit an attitude that is slightly disrespectful. I hope it will be the last time.
@atdiar
The slice type constructor implicitly requires that T is nilable/zeroable (because a slice can be cleared) . I don't see why such a check would be infeasible.
So, you are saying AddNElems
where T
is any
will not work with "non-nilable interfaces" - is that correct?
Where T is constrained by any? Yes it won't work. (in your example, because T also is declared as a slice type) If you have a non-nilable union type U, U wouldn't be usable in a slice. However interface{U | nil} should be usable as a type argument.
Just a reminder to keep the discussion here respectful. You know who you are. Thanks.
@atdiar
So, to be clear, you essentially want a separate class of types (meta types?) similar to existing, but with additional and very specific restrictions to available operations on this "class" of types.
I mean, the whole point of any
is that it can satisfy any type (and by extent, being type constraint, works for any type). With what you are proposing, Go type system will be divided not only by two "type hierarchies" but also two "type constraint hierarchies". Essentially, this means we will have two entirely separate type systems which are not interchangeable and have very different low level semantics. Which, in turn. essentially means you will have a two different languages in one - first one assumes that zero state is valid (starting from slices and ending with "reflect") and the second one demands initialization and disallows the set of operations like make([]T, n)
and such.
The complexity of implementing this, in the end, equals to creating a new language, so the question becomes - why bother with adding this to Go? What I'm trying to say, is that no matter how we don't like "zero value" in specific situations, it is one of the fundamentals the language is built on. We can adjust sum types to work with it, but we cannot break it or make a parallel mechanism for sum types - this will either be a fundamentally breaking change (the road Dart 2.0 took BTW) or unsound type system. I don't think we want either.
This is off-topic now, please create another proposal if you have ideas on adding non-zero variables to the language. Thanks.
Understood. I'll create another prospective issue. To be clear one more time, this is still not "adding non-zero variables" , it is about definite (un)assignment. Zero values are fine, but the runtime.Type of the zero value is not necessarily in the type set which is what I'm trying to address. Doesn't seem that off-topic to me but fair enough. Cheers.
I think one of the first things some Go developers would try to do with this feature is implement a generic Option
type. Here's a brief exploration of that idea.
My first thought was that it would look like this:
type None struct{}
type Option[T any] interface{None | T}
But the first problem here is that a type parameter can't be used in a type list: the definition above results in a MisplacedTypeParam
error.
The second problem is that, since an interface value can be nil
, a variable of type Option[T]
could either nil
, or None
, or a value of type T
. So it would be a sort of optional optional.
In fact to define an Optional int64
type this would be sufficient:
type OptionalInt64 interface{int64}
This would actually have many of the properties I would be looking for in an option type:
nil
("None")int64
value would be stored directly (avoiding memory allocations)I have sometimes used sql.Null*
types such as sql.NullInt64
as generic optional types (even, sadly, in code that doesn't otherwise deal with sql) because they have the above advantages. In fact if this proposal was implemented with values stored directly, then OptionalInt64
would be stored as the equivalent of sql.NullInt64
.
But OptionalInt64
would add some type safety: you couldn't treat an OptionalInt64
as an int64
without first guaranteeing it was not nil
(right?).
A standard generic Option type was discussed in #48702. From what I can see the proposal was rejected because of unanswered questions, and not necessarily because a standard optional type was not desirable.
Difficult question, but I think it may contribute to the “costs” of this proposal: How serious do you estimate the danger that people start using sum types instead of error types as return types? Would this be even feasible?
@bronger I don't think this proposal is particularly useful for that - and insofar as it is, you could get basically the same effect without it.
One thing people want from a Result[T]
type is that it can either be an error
or a T
. But with this proposal, there's always a third option: It could be nil
. So, right off the bat this proposal gives weaker guarantees than what people really want.
Then you'd have to jump through a couple of hoops to make such a Result
type actually safe. There are some obstacles:
T
could implement error
T
nor error
can be union terms (the former because it's a type parameter, the latter because it has methods)Result[T]
using type-assertions, so the cases must be disjoint, to be type safeSo you'd get something like
type Error struct { E error }
type Success[T any] struct { Val T }
type Result[T any] interface { Error | Success[T] }
There's a lot of overhead in using this, over returning (T, error)
- not just because you need to type-assert, but you also need to wrap and unwrap the individual structs.
And the value you get is saying that a Result[T]
is either an Error
or Succes[T]
, fair enough. But what does that get you, except documentation? The compiler won't actually type-check that you correctly type-switch on it exhaustively. So, how is this really any better than just returning an any
and documenting that it's either an Error
or a Success
(or construct a different non-union interface for wrapping)?
I think the value of this proposal can only really come in enforcing constraints on inputs, really. There is some value in knowing that you aren't being given anything but one of these N things. The information that a function only returns a finite set of types isn't super useful without further infrastructure (like match statements and exhaustiveness checks and the like).
One advantage, to me, is if it makes it easier to plug things that return Result[T]
into things that accept Result[T]
(or, possibly, ...Result[T]
) than it is with (T, error)
. Same for Option[T]
vs (T, bool)
.
This is a speculative issue based on the way that type parameter constraints are implemented. This is a discussion of a possible future language change, not one that will be adopted in the near future. This is a version of #41716 updated for the final implementation of generics in Go.
We currently permit type parameter constraints to embed a union of types (see https://go.dev/ref/spec#Interface_types). We propose that we permit an ordinary interface type to embed a union of terms, where each term is itself a type. (This proposal does not permit the underlying type syntax
~T
to be used in an ordinary interface type, though of course that syntax is still valid for a type parameter constraint.)That's really the entire proposal.
Embedding a union in an interface affects the interface's type set. As always, a variable of interface type may store a value of any type that is in its type set, or, equivalently, a value of any type in its type set implements the interface type. Inversely, a variable of interface type may not store a value of any type that is not in its type set. Embedding a union means that the interface is something akin to a sum type that permits values of any type listed in the union.
For example:
The types
MyInt
andMyFloat
implementI1
. The typeMyOtherInt
does not implementI1
. None ofMyInt
,MyFloat
, orMyOtherInt
implementI2
.In all other ways an interface type with an embedded union would act exactly like an interface type. There would be no support for using operators with values of the interface type, even though that is permitted for type parameters when using such a type as a type parameter constraint. This is because in a generic function we know that two values of some type parameter are the same type, and may therefore be used with a binary operator such as
+
. With two values of some interface type, all we know is that both types appear in the type set, but they need not be the same type, and so+
may not be well defined. (One could imagine a further extension in which+
is permitted but panics if the values are not the same type, but there is no obvious reason why that would be useful in practice.)In particular, the zero value of an interface type with an embedded union would be
nil
, just as for any interface type. So this is a form of sum type in which there is always another possible option, namelynil
. Sum types in most languages do not work this way, and this may be a reason to not add this functionality to Go.As an implementation note, we could in some cases use a different implementation for interfaces with an embedded union type. We could use a small code, typically a single byte, to indicate the type stored in the interface, with a zero indicating
nil
. We could store the values directly, rather than boxed. For example,I1
above could be stored as the equivalent ofstruct { code byte; value [8]byte }
with thevalue
field holding either anint
or afloat64
depending on the value ofcode
. The advantage of this would be reducing memory allocations. It would only be possible when all the values stored do not include any pointers, or at least when all the pointers are in the same location relative to the start of the value. None of this would affect anything at the language level, though it might have some consequences for thereflect
package.As I said above, this is a speculative issue, opened here because it is an obvious extension of the generics implementation. In discussion here, please focus on the benefits and costs of this specific proposal. Discussion of sum types in general, or different proposals for sum types, should remain on #19412 or newer variants such as #54685. Thanks.