Description
This is a speculative issue for discussion about an aspect of the current generics design draft. This is not part of the design draft, but is instead a further language change we could make if the design draft winds up being adopted into the language.
The design draft describes adding type lists to interface types. In the design draft, an interface type with a type list may only be used as a type constraint. This proposal is to discuss removing that restriction.
We would permit interface types with type lists to be used just as any other interface type may be used. A value of type T
implements an interface type I
with a type list if
- the method set of
T
includes all of the methods inI
(if any); and - either
T
or the underlying type ofT
is identical to one of the types in the type list ofI
.
(The latter requirement is intentionally identical to the requirement in the design draft when a type list is used in a type constraint.)
For example, consider:
type MyInt int
type MyOtherInt int
type MyFloat float64
type I1 interface {
type MyInt, MyFloat
}
type I2 interface {
type int, float64
}
The types MyInt
and MyFloat
implement I1
. The type MyOtherInt
does not implement I1
. All three types, MyInt
, MyOtherInt
, and MyFloat
implement I2
.
The rules permit an interface type with a type list to permit either exact types (by listing non-builtin defined types) or types with a particular structure (by listing builtin defined types or type literals). There would be no way to permit the type int
without also permitting all defined types whose underlying type is int
. While this may not be the ideal rule for a sum type, it is the right rule for a type constraint, and it seems like a good idea to use the same rule in both cases.
Edit: This paragraph is withdrawn. We propose further that in a type switch on an interface type with a type list, it would be a compilation error if the switch does not include a default
case and if there are any types in the type list that do not appear as cases in the type switch.
In all other ways an interface type with a type list 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 when using such a type as a type constraint. This is because in generic code 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 list, 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 a type list 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, namely nil
. Sum types in most languages do not work this way, and this may be a reason to not add this functionality to Go.
As I said above, this is a speculative issue, opened here because it is an obvious extension of the generics design draft. 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. Thanks.
Activity
Merovius commentedon Sep 30, 2020
I don't understand this. If all types in the type list appear as cases, the default case would never, trigger, correct? Why require both?
Personally, I'm opposed to requiring to mention all types as cases. It makes it impossible to change the list. ISTM at least adding new types to a type-list should be possible. For example, if
go/ast
used these proposed sum types, we could never add new node-types, because doing so would break any third-party package usingast.Node
. That seems counterproductive.I think requiring a default case is a good idea, but I don't like requiring to mention all types as cases.
There is another related question. It is possible for such a sum-value to satisfy two or more cases simultaneously. Consider
I assume that the rules are the same as for type-switches today, which is that the syntactically first case is matched? I do see some potential for confusion here, though.
griesemer commentedon Sep 30, 2020
[edited]
@Merovius It does say "...and if there are any types in the type list that do not appear as cases in the type switch." Specifically, there is no comma between "default case" and "and". Perhaps that is the cause for the confusion?
Regarding the multiple cases scenario: I think this would be possible, and it's not obvious (to me) what the right answer here would be. One could argue that since the actual type stored in
x
isA
that perhaps that case takes precedence.tooolbox commentedon Sep 30, 2020
Makes sense. I can see how the language is a little ambiguous, the point is it's a compile error if both of those conditions exist.
It occurred to me that tooling could spot when a type switch branch was invalid, i.e. the interface type list only contains A and B and your switch checks for C, but it seems best to not make that a compiler error. A linter could warn about it, but being overly restrictive here might harm backwards-compatibility.
I think it makes the most sense for the type switch to behave consistently. It's not clear to me how the type switch would be any different except that the interface being switched on has a type list. You can know at compile-time what branches should be in the switch, but that's it.
Overall I'm in favor, I think the proposal is right on the money. They function like any other interface, (no operators) and zero value is
nil
. Simple, consistent, unifies semantics with the Generics proposal. 👍Merovius commentedon Sep 30, 2020
@griesemer Ah, I think I understand now. I actually misparsed the sentence. So AIUI now, the proposal is to require either a default case or to mention all types, correct?
In that case, the proposal makes more sense to me and I'm no longer confused :) I still would prefer to require a default case, though, to get open sums. If it is even allowed to not have a default case, it's impossible to add new types to the type-list (I can't know if any of my reverse dependencies does that for one of my exported types, so if I don't want to break their compilation, I can't add new types). I understand that open sums seem less useful to people who want sum types, though (and I guess that's at the core of why I consider sum types to be less useful than many people think). But IMO open sums are more adherent to Go's general philosophy of large-scale engineering and the whole gradual repair mechanism - and also more useful for almost all use-cases I see sum types suggested for. But that's just my 2¢.
griesemer commentedon Sep 30, 2020
@Merovius Yes, your new reading is correct.
mvdan commentedon Sep 30, 2020
Could you clarify why
nil
should always be an option in such sum types? I understand this makes them more like a regular interface, but I'm not sure if that consistency benefit outweighs how it makes them less useful.For example, they could be left out by default, or included by writing
nil
oruntyped nil
as one of the elements in the type list.I understand that the zero value gets trickier if we remove the possibility of nil, which might be the reason behind always including nil. What do other languages do here? Do they simply not allow creating a "zero value" of a sum type?
mvdan commentedon Sep 30, 2020
To add to my comment above - @rogpeppe's older proposal in #19412 (comment) does indeed make
nil
opt-in, and the zero value of the sum type becomes the zero value of the first listed type. I quite like that idea.jimmyfrasche commentedon Sep 30, 2020
@mvdan as far as I'm aware other languages with sum types do not have the notion of a zero value and either require a constructor or leave it undefined. It's not ideal to have a nil value but getting something that works both as a type and a metatype for generics is worth the tradeoff, imo.
Merovius commentedon Sep 30, 2020
I guess (as a nit)
nil
should also be a required case if no default case is given, if we makenil
a valid value.jimmyfrasche commentedon Sep 30, 2020
So this https://go2goplay.golang.org/p/5L7T8G9rfLD would print "something else" under the current proposal, correct? The only way to get that value is reflect?
96 remaining items
beoran commentedon Jan 23, 2021
One solution for the type switch problem for the union proposed in this issue wiuld be to require the type switch to consist of all members of the union, each nominally, nothing more and nothing less. If any member is not exported then the type switch is only allowed in the defining package. This idea is simple and Go-like, I think.
millergarym commentedon Jan 27, 2021
@ianlancetaylor @griesemer
Based on Go's orthogonality principle and personal research / experience of simple algebraic type system, I strongly feel a discriminated union type would be superior to type lists (or at minimum the list part).
To fully replace type lists and not just the list part would require changes to interfaces types to match fields as well as methods (unfortunately this change to the language seems to have already been ruled out #23796).
Below is an example of rewriting the initial example using a type union, only replacing the list is types lists, followed by an example of replacing type lists with union type where interfaces can match fields.
Both these examples achieve the semantically of types lists, but also allow for discriminated unions to be used in other places.
Example of replacing list with union type
Example of replacing type lists with union type
Above the unions are using embedded branches (a branch being analogous to a field in a struct or a signature in an interface).
Unions would be syntactically similar to interfaces and structs, but with different semantics.
Discriminated unions would be similar to interfaces in their implementation.
They both can be thought of as "fat pointers" containing 1. a pointer the data and 2. a discriminator specifying which data is being pointed at.
Also similar to interfaces a type switch over the different types is possible.
They differ from interfaces as they are
closed
(new branches can't be added out side of their declaration) and type switches must be exhaustive else the compiler will complain.I appreciated this comment is unlikely to get much traction as the prevailing opinion seems to be strongly against unions and fields in interfaces. I was motivated to write this as Robert explicitly called into question type lists in interfaces.
Not a conclusive reason, but discriminated unions exist in many other languages.
bcmills commentedon Jan 27, 2021
@millergarym, alternatives involving discriminated unions have been discussed at length on #19412.
This issue is specifically focused on generalizing the type-list interfaces suggested by #43651. I don't think that the semantics required by #43651 are compatible with the
union
semantics you describe.millergarym commentedon Jan 28, 2021
@bcmills sorry, didn't read the initial message well enough.
Picking up on
and responding to
In simple algebraic type systems unions are sum types.
If type lists are only kinda sum types it will be another wart on the Go type system.
As pointed out in Featherweight Go, Go has two types.
It only needed one.
Type lists, as proposed are exacerbating this design flaw by widening the gap between interfaces and structs.
I appreciate the issues are deeply embedded in the Go type system
eg
But isn't generic and Go2 an opportunity try tack these issues?
ianlancetaylor commentedon Jan 28, 2021
The current generics proposal is explicitly backward compatible.
It is unlikely that Go will ever make large backward incompatible changes, as discussed at https://go.googlesource.com/proposal/+/refs/heads/master/design/28221-go2-transitions.md.
fzipp commentedon Feb 11, 2021
I already posted this in the thread of the type parameters proposal, but I think here is the correct place.
Type lists + interfaces instead of type lists in interfaces
Current type parameters design
Usage:
Suggested change
Usage:
Interfaces are left untouched.
The resulting sum type stands on its own feet, and it's not just a pale imitation of a sum type.
The
(
...)
notation to shift the type matching rules for constraint usage is admittedly a bit odd and subtle,e.g.
[T (constraints.Ordered)]
vs.[T constraints.Ordered]
.ianlancetaylor commentedon Apr 2, 2021
Retracting in favor of #45346 (which may in time lead to another proposal similar to this one, but different).
ianlancetaylor commentedon Jan 5, 2023
I filed #57644 to update this for the final implementation of generics adopted into the language.