-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: add sum types / discriminated unions #19412
Comments
This has been discussed several times in the past, starting from before the open source release. The past consensus has been that sum types do not add very much to interface types. Once you sort it all out, what you get in the end is an interface type where the compiler checks that you've filled in all the cases of a type switch. That's a fairly small benefit for a new language change. If you want to push this proposal along further, you will need to write a more complete proposal doc, including: What is the syntax? Precisely how do they work? (You say they are "value types", but interface types are also value types). What are the trade-offs? |
See https://www.reddit.com/r/golang/comments/46bd5h/ama_we_are_the_go_contributors_ask_us_anything/d03t6ji/?st=ixp2gf04&sh=7d6920db for some past discussion to be aware of. |
I think this is too significant a change of the type system for Go1 and there's no pressing need. |
Thanks for creating this proposal. I've been toying with this idea for a year or so now. Sum types in GoA sum type is represented by two or more types combined with the "|"
Values of the resulting type can only hold one of the specified types. The As a special case, "nil" can be used to indicate whether the value can For example:
The method set of the sum type holds the intersection of the method set Like any other interface type, sum type may be the subject of a dynamic The zero value of a sum type is the zero value of the first type in When assigning a value to a sum type, if the value can fit into more For example:
would result in a value with dynamic type int, but
would result in a value with dynamic type float64. ImplementationA naive implementation could implement sum types exactly as interface For example a sum type consisting only of concrete types without pointers For sum-of-struct-types, it might even be possible to use spare padding |
@rogpeppe How would that interact with type assertions and type switches? Presumably it would be a compile-time error to have a |
For type switches, if you have
and you do:
and t contains an interface{} containing an int, does it match the first case? What if the first case is Or can sum types contain only concrete types? What about
what is t's type? Or is that construction forbidden? A similar question arises for |
Yes, I think it would be reasonable to have a compile-time error That means that you can get the compiler to error if you have:
and you change the sum type to add an extra case. |
t can't contain an interface{} containing an int. t is an interface Sum types can match interface types, but they still just get a concrete
According to the proposal above, you get the first item In fact interface{} | nil is technically redundant, because any interface{} For []int | nil, a nil []int is not the same as a nil interface, so the |
The type T []int | nil
var x T = nil would imply that That value would be distinct from the nil var y T = []int(nil) // y != x |
Wouldn't nil always be required even if the sum is all value types? Otherwise what would It seems overly subtle. Exhaustive type switches would be nice. You could always add an empty |
The proposal says "When assigning a value to a sum type, if the value can fit into more So, with:
x would have concrete type []int because nil is assignable to []int and []int is the first element of the type. It would be equal to any other []int (nil) value.
The proposal says "The zero value of a sum type is the zero value of the first type in
No, it would just be the usual interface nil value in that case. That type (interface{} | nil) is redundant. Perhaps it might be a good idea to make it a compiler to specify sum types where one element is a superset of another, as I can't currently see any point in defining such a type. |
That is an interesting suggestion, but since the sum type must record somewhere the type of the value that it currently holds, I believe it means that the zero value of the sum type is not all-bytes-zero, which would make it different from every other type in Go. Or perhaps we could add an exception saying that if the type information is not present, then the value is the zero value of the first type listed, but then I'm not sure how to represent |
So @ianlancetaylor I believe many functional languages implement (closed) sum types essentially like how you would in C
if That would make union's value types instead of special interfaces, which is also interesting. |
Is there a way to make the all zero value work if the field which records the type has a zero value representing the first type? I'm assuming that one possible way for this to be represented would be: type A = B|C
struct A {
choice byte // value 0 or 1
value ?// (thing big enough to store B | C)
} [edit] Sorry @jimmyfrasche beat me to the punch. |
Is there anything added by nil that couldn't be done with
? That seems like it avoids a lot of the confusion (that I have, at least) |
Or better
that way you could type switch on |
@jimmyfrasche |
@bcmills It wasn't my intent to claim otherwise—I meant that it could be used for the same purpose as differentiating a lack of value without overlapping with the meaning of nil in any of the types in the sum. |
@rogpeppe what does this print?
I would assume "Reader" |
@jimmyfrasche I would assume (And I would also expect sums which include only interface types to use no more space than a regular interface, although I suppose that an explicit tag could save a bit of lookup overhead in the type-switch.) |
@bcmills it's the assigment that's interesting, consider: https://play.golang.org/p/PzmWCYex6R |
@ianlancetaylor That's an excellent point to raise, thanks. I don't think it's hard to get around though, although it does imply that my "naive implementation" suggestion is itself too naive. A sum type, although treated as an interface type, does not have to actually contain direct pointer to the type and its method set - instead it could, when appropriate, contain an integer tag that implies the type. That tag could be non-zero even when the type itself is nil. Given:
the runtime value of x need not be all zeros. When switching on the type of x or converting Another possibility would be to allow a nil type only if it's the first element, but
|
Yes.
I don't get this. Why would "this [...] have to be valid for the type switch to print ReadCloser" When there are several interface types in a sum, the runtime representation is just an interface value - it's just that we know that the underlying value must implement one or more of the declared possibilities. That is, when you assign something to a type (I1 | I2) where both I1 and I2 are interface types, it's not possible to tell later whether the value you put into was known to implement I1 or I2 at the time. |
If you have a type that's io.ReadCloser | io.Reader you can't be sure when you type switch or assert on io.Reader that it's not an io.ReadCloser unless assignment to a sum type unboxes and reboxes the interface. |
Going the other way, if you had io.Reader | io.ReadCloser it would either never accept an io.ReadCloser because it goes strictly right-to-left or the implementation would have to search for the "best matching" interface from all interfaces in the sum but that cannot be well defined. |
@meln5674 |
Performance, type-safety, and improved developer experience. "We don't need
X because we have reflection" is a bit of a cop-out, don't you think? Go's
reflection is certainly powerful, but that doesn't mean it should be
the hammer for every nail. The same argument could easily be (and was)
applied to generics themselves. As I discussed in the linked proposal, yes,
it is currently possible to emulate tagged unions in Go, but all possible
mechanisms have downsides which render Go a poor choice for problems
that are best solved with them, and it would be possible to add with
minimal changes.
|
@meln5674 type Float tagged.Union[[8]byte, struct {
Bits32 tagged.As[Float, float32]
Bits64 tagged.As[Float, float64]
}]
var FloatWith = tagged.Fields(Float{})
var pi Float
pi = FloatWith.Bits32.New(math.Pi)
pi = FloatWith.Bits64.New(math.Pi)
switch tagged.FieldOf(pi) {
case FloatWith.Bits32.Field:
var f32 float32 = FloatWith.Bits32.Get(pi)
fmt.Println(f32)
case FloatWith.Bits64.Field:
var f64 float32 = FloatWith.Bits64.Get(pi)
fmt.Println(f64)
} This has good performance, it is type-safe and IMO has a good developer experience. Best of all, unlike any of the proposals here, it is supported today. You can use it. You can customise it. It's only a couple hundred lines. I haven't included reflection support or marshalling in this implementation but it is easy to add (I've done it before). If the unsafe code in this particular implementation makes you nervous, you can implement unions with an underlying interface/any. Depends on your performance use-case. |
@Splizard Thanks for the library! However, I do want to point out that there is a distinction between having something available as a library API and having it natively supported by the language (or its stdlib). In the former case, a tagged union will be much less likely to appear in public API surfaces. |
While that's an interesting approach, and perhaps this isn't the appropriate place for a code review, I can see a number of things that would steer me away from using this in production code. First, this isn't type-safe. I suspect that you and I are using that word to mean different things. For example, if you were to mix up the cases on a switch, that would not be a compiler error, but a runtime panic. Compare this to a Please don't interpret this post as "this is everything wrong with your code, your library is bad, and you should feel bad", that is absolutely not what I'm trying to say, this is quite a clever piece of code, and I imagine it was a useful exercise to write it, but rather, these are the things that are an inevitable consequence of implementing tagged unions using workarounds, because the language lacks appropriate facilities to implement them correctly. The entire purpose of this, my, and other similar proposals is to add those facilities so that they don't need to be workarounds, plus all the benefits of a unified ecosystem that rami3l touched on. If you genuinely believe that this is the correct approach, then you should propose adding something based on it to the standard library, and if you do, I wish you the best of luck. |
@meln5674 @Splizard I don't think this discussion is particularly helpful. In particular, I'll note that there is a certain amount of snark entering here. Let's just note that 1. there are certain ways to work around the lack of sum/union types in Go, but 2. this issue is about adding a first-party, language-level support for them - which is a feature request that has a place (even though I'm personally not that on board with its usefulness either). |
Adding a new keyword does not seem to be completely ruled out, as noted by @rsc. So if we made unions separate with the simple "zero value based on first type" rule:
Then these
could be short form for:
Within interfaces the order of types in the unions wouldn't matter. There would be only one source of type options (union), and it would integrate nicely into other places, such as interfaces as demonstrated above. We could still generalize interfaces as proposed in #57644 later, if we feel that we want to have nilable unions, or we could leave them the way they are today. |
@gophun The main argument against adding a separate union concept like that (regardless of whether or not that requires a new keyword - see #54685 for a design that doesn't require a new keyword) is the conceptual overlap with interfaces. That is, the main reason not to do it, is that we'd then have both unions in general interfaces and unions-as-types, which serve very similar functions, but are different concepts, which is not very orthogonal. (Also, obligatory note that all of this has been discussed above, at length) |
I had hoped that I demonstrated that there is no conceptual overlap with interfaces, but a conceptual composition. They are orthogonal concepts, but they can be composed, given these short-hand rules:
|
The conceptual overlap is that both represent a list of concrete types. Preventing them from be used interchangeably doesn't prevent that overlap in concept - it's what makes it confusing. |
|
Ideally, I would prefer just |
Can anybody help me find a potential parsing ambiguity? These seem to be possible to distinguish from the bitwise
|
@gophun I agree that FTR you might well disagree with the judgement or not find the argument of conceptual overlap persuasive. But that is the reason we have not accepted any proposal for a separate union/sum-type construct. And if we accepted using a separate concept, I'd be strongly in favor of making it actual sum types, not union types - if we have to pay the cost of overlap and a new concept anyways, we might as well make it the better one. As for the ambiguity, I don't believe there is an ambiguity per se, but that syntax might require more lookahead than Go has traditionally been comfortable with. |
@Merovius |
Fair enough. I still don't think that invalidates the argument that having to separate ways to express a union of types is a downside of that idea. Note that union-elements where not introduced to be able to write Again, it's fine to not find these arguments persuasive. But they are the reason we haven't done this yet and it might be beneficial to try to understand them, instead of trying to argue them away. |
As you know, since you have followed my train of thought, I actually prefer the variant without the |
@gophun Okay. I can see that you are not interested in understanding the context of this discussion. |
To recap, from my perspective:
The point is, either a) you are suggesting a new thing, which has significant conceptual overlap with union elements in interfaces, or b) you are suggesting to assign a type-meaning to union elements in interfaces, in which case you have the zero-value and/or ordering problem. This isn't a syntax problem. It's a problem with the concepts. |
It's true that my thoughts are evolving, that's what a discussion is good for. My goal is to find a solution that has the desired non-nilable property of a union type, integrates well with the existing language, and that does not have more conceptual overlaps than what already exists.
No, it does not bring us back to that.
|
Fair enough, we are talking about inventing a new thing with conceptual overlap. TBQH it is quite confusing that you are choosing to focus on a syntax that gives the new thing also syntactical overlap with general interfaces. For example, if I really don't believe you can escape the dichotomy I lined out above. Either union types are the same as general interfaces - in which case you can't rely on order to answer the zero value problem - or they are not - in which case there now are two very similar, but distinct concepts in the language. This isn't really a thing to "solve". Instead, if we are to add union (or sum) types to Go, we'll just have to accept the downsides of one of those branches. |
This is true; I hadn't seen this. So consider the idea scrapped. |
Can we just get optionals please, I know it is a special case for enums but no enums is fine can we just get optionals. |
What does it take to make a final decision on an issue like this? |
@melodyogonna currently the proposal that has most chances of being adopted is #57644 But there are still unanswered question: what to do with |
Would either of these approaches lend themselves to the type of enums used for Option --> None/Some and Result --> Ok/Err in Rust? I get the impression that's what advocates of enums in Go are really after (understandably). |
This is a proposal for sum types, also known as discriminated unions. Sum types in Go should essentially act like interfaces, except that:
Sum types can be matched with a switch statement. The compiler checks that all variants are matched. Inside the arms of the switch statement, the value can be used as if it is of the variant that was matched.
The text was updated successfully, but these errors were encountered: