-
Notifications
You must be signed in to change notification settings - Fork 17.8k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
spec: generics: use type sets to remove type keyword in constraints #45346
Comments
Change https://golang.org/cl/306689 mentions this issue: |
At first glance, this seems okay to me. Some comments:
|
Part 3. of @Merovius's comment nicely addresses the issue in Identifying the matched predeclared type of the Type Parameters Proposal. |
Does this mean that we can't define something like: type Dictionary[K any, V any] interface {
~map[K]V
} or type GenChan[T any] interface {
~chan T
} ? |
Thanks, @Merovius for the excellent initial feedback. Some comments to your points:
|
@DmitriyMV No, to the contrary. Such constraints are explicitly permitted because the underlying type of |
Thanks! So what are those
types for example? |
As I described in #43651 (comment) and #44235, I think it creates a great pedagogical load to reuse the name "interface" for concepts that are not actually types. So, to me, the strength of this proposal is in addressing the concerns raised in #41716. I believe that the right decision is to leave as few releases as possible, ideally zero, between the release of generics and the "possible future step" of using this syntax (or another, if this is ultimately rejected) for sum types. The longer we go without being able to instantiate every type parameter with its own definition, the more tutorials, books, guidelines, and style documents will be written based on it being impossible.
Conversely, I like that the default behavior here preserves Go's strong one-to-one relationship between names and types (aside from type aliases, which were introduced late and are usually discouraged). |
@DmitriyMV Any defined type has an underlying type that is not itself. Look also for the definition of underlying types. For instance, type myint int is a defined type whose underlying type is It's important to fully understand the notion of |
@griesemer To be clear: I'm advocating not for "not promising to return an error", but for "promising not to return an error". That is, the compiler should consider an empty type-set valid, but vet might flag it :) |
I'm wondering whether this proposal has any rough edges related to instantiation of constraints using interface types. Say I do something like below:
Are either of the following valid:
edit: my reading is yes and they that are both valid. |
It's kind of a bias from other languages, but my first inclination is to read var Interface fmt.Stringer // Valid values limited to those in the set of fmt.Stringer.
var Concrete int // Valid values limited to those in the set of int. The second would conflict with the embedding of types if Kind of makes me wonder about the potential legality in the future of Disclaimer: I am not necessarily advocating for |
Speaking as just a random developer who is pretty new to Go--so take this with a grain of salt--I can't help but feel like the whole constraint interfaces thing is just a complicated end run to avoid having to add operator overloading to the language. If we had the ability to express that an interface includes specific operators (with whatever syntactic form makes it unambiguous), then we don't need type sets / constraint interfaces to enumerate types. The generics proposal, with or without this type sets proposal, takes Go further away from structural typing. Edit: and honestly, we don't even need operator overloading to keep the language unambiguous. We just need a way to express that an interface includes certain operators. Then |
Is there a practical use for having both That is: |
For numerical work you would like to implement |
I still can't see the benefit of excluding derived types from such an implementation. If a type has the underlying type of |
Yeah, I can see your point. The |
@Merovius Isn't it better to be accidentally too restricting and have the possibility to lift the restriction later than to be accidentally too permissive? Are there situations where changing T to ~T is a breaking change for the consumer of a parametrized type or function? |
The case for including both |
At the risk of sounding +1, I would like to say that this seems to be a strong step forward, and that Ian and Robert have my thanks for their ongoing efforts to make generics a reality in a way that fits with Go’s ethos and that gophers can learn easily and use effectively. No objections to the proposal, although I agree with @zephyrtronium that it would be ideal to narrow the gap between regular interfaces and constraint interfaces, hopefully to nothing, before generics are released, so I look forward to further iteration. |
I'm curious whether it would be possible to allow approximations of structs to match not only the underlying type of a particular struct, but also to allow matches for structs that have at least the exact fields that are listed as the approximate struct element? type Fooer interface {
~struct { Foo int; Bar string }
}
type MyFoo struct {
Foo int
Bar string
Baz float64
} In the above snippet, there is a potential for allowing types like MyFoo to satisfy the Fooer constraint. This could be quite useful, as structs are the only class of types that vary wildly. I know of at least one other language that allows for a similar expression |
Maybe this should be added to proposal in a form of explanation "aka having |
@bserdar AFAICT the main reason to have both is the possibility of expanding to sum types. Constraints almost always want to use @fzipp I'm not sure. It might not be, especially if we only consider the proposal as-is with this change. Note that it's also possible to use constraints defined by other packages for your own function, so it's not just the consumers of a generic function that are affected, but also authors. A function that uses a constraint and type-asserts an argument would be an example of a breakage, when this constraint is relaxed. But this is shot from the hip - I have no idea how real/practically relevant this is. It becomes more relevant if we expand to sum types though. @urandom It's definitely possible. I'm not sure it's a good idea though. It's a pretty significant change in the meaning of |
Some questions / remarks:
This has the benefit of not introducing a new token and does not confuses to read as "not float32 or not float64". Also T- could at some point refer to the underlying type of T, which might be useful to express at some point.
Do this not work?
Ie.
When would you ever need that? Instead, would it be possible / sensible to move the "~" to the parameter type side? I.e.
Could something like this work to declare a type set/list?
To me, an interface is used when you care about functionality only (methods) and not about representation (actual type). So the word "interface" does not resonate very well to me with a type list (plus the interface concept of Go is already quite involved). |
There's an interesting comment on r/golang, bringing up a parallel of this proposed syntax resembling the (also newline-based) boolean/set logic syntax of |
@markusheukelom As for question 3: |
Based on the discussion above, this proposal seems like a likely accept. |
No change in consensus, so accepted. 🎉 |
We propose clarifications for the semantics of constraint satisfaction in the generics proposal. We also propose changing the syntax of type lists to remove the
type
keyword and to explicitly specify when type arguments should match on underlying types.The changes this would make to the current generics proposal document can be seen in https://golang.org/cl/306689.
Background
The current generics proposal proposes a new syntax for type lists within interfaces. A type list within an interface is the keyword
type
followed by a list of types separated by commas. Type lists are only permitted in interface types that are used as type constraints. For example:A type argument matches a constraint with a type list if
This rule was adopted in part to support permitting type lists in ordinary interface types, not only in constraints. However, discussion has made clear that the rule is too subtle. This suggests that it is too subtle not just for use in ordinary interface types, but also for use in constraints.
The behavior when embedding interfaces with type lists is also subtle.
We can do better.
Type sets
We start by defining a type set for all types. We will define what it means for a type to implement an interface in terms of type sets, resulting in a behavior that is equivalent to the current definition based on method sets.
Every type has an associated type set. The type set of an ordinary non-interface type
T
is simply the set{T}
which contains justT
itself. The type set of an interface type (in this section we only discuss ordinary interface types, without type lists) is the set of all types that declare all the methods of the interface.Note that the type set of an interface type is an infinite set. For any given type
T
and interface typeIT
it's easy to tell whetherT
is in the type set ofIT
(by checking whether all methods ofIT
are declared byT
), but there is no reasonable way to enumerate all the types in the type set ofIT
. The typeIT
is a member of its own type set because an interface inherently declares all of its own methods. The type set of the empty interfaceinterface{}
is the set of all possible types.With this idea of type sets, we can restate what it means for a type
T
to implement an interface typeIT
:T
implementsIT
ifT
is a member of the type set ofIT
. Since the type set ofIT
is the set of all types that declare all the methods of the interface,T
is a member of the type set ofIT
if and only if the method set ofT
is a (possibly improper) superset of the method set ofIT
, which is the standard definition of implementing an interface.Now let's consider embedded interfaces. For a case like
type O1 interface{ E }
, the type set ofO1
is the same as the type set ofE
. The casetype O2 interface{ E1; E2 }
is more interesting: the type set ofO2
is the intersection of the type sets ofE1
andE2
. To see this, observe that the type set ofE1
is the set of all types that implement all the methods ofE1
, and similarly forE2
. What we want for the type set ofO2
is the set of all types that implement all the methods ofO2
. The methods ofO2
are all of the methods ofE1
combined with all of the methods ofE2
. The set of types that implement all the methods of bothE1
andE2
is the intersection of the type sets ofE1
andE2
.Note that listing a method in an interface type definition in the usual way is, from a type set perspective, indistinguishable from embedding an interface that declares just that method. Although a method by itself is not a type, for our purposes we can say that the type set for a method listed explicitly in an interface type definition is exactly the type set of an interface type with only that method: the set of all types that implement that method. The advantage of doing this is that we can now say that the type set of an interface type is exactly the intersection of the type sets of each element listed in the interface.
We've now described type sets, and we've explained the meaning of implementing an interface in terms of type sets. None of this changes the language in any way, but it serves as background and motivation for the next steps.
Proposal
We propose to replace type lists as defined by the generics proposal with three new, simpler, ideas.
An interface type that is used as a constraint, or that is embedded in a constraint, is permitted to embed some additional constructs that we will call interface elements. An interface element can be:
With these new elements we will be able to state simply that a type argument
A
satisfies a constraintC
exactly whenA
implements the interface typeC
, or, in terms of type sets, exactly whenA
is a member of the type set ofC
.First, we propose that an interface type used as a constraint is permitted to embed a non-interface type. For example:
type Integer interface{ int }
. As discussed in the previous section, the type set of an interface type is the intersection of the type sets of the elements of the interface. The type set ofint
is simply{int}
. This means that the type set ofInteger
is also{int}
.This constraint can be satisfied by any type that is a member of the set
{int}
. There is exactly one such type:int
.Of course, that is useless by itself. For constraint satisfaction, we want to be able to say not just
int
, but "any type whose underlying type isint
." To implement this, we propose a new syntactic construct, which may be embedded in an interface type used as a constraint. This is an approximation element, written as~T
. The type set of an approximation~T
is the set of all types whose underlying type isT
. An approximation~T
is only valid if the underlying type ofT
is itselfT
; this is discussed in more detail below.For example:
type AnyInt interface{ ~int }
. The type set of~int
, and therefore the type set ofAnyInt
, is the set of all types whose underlying type isint
. For example, ifMyInt
is defined astype MyInt int
, thenMyInt
used as a type argument will satisfy the constraintAnyInt
.The final step is another new syntactic construct that may be embedded in an interface type used as a constraint: a union element. A union element is written as a sequence of types or approximation elements separated by vertical bars (
|
). For example:int | float32
or~int8 | ~int16 | ~int32 | ~int64
. The type set of a union element is the union of the type sets of each element in the sequence. The types and elements listed in a union must all be different: no two types may be identical, and no two approximation elements~T1
and~T2
may haveT1
identical toT2
. For example:The type set of this union element is the set
{int, int8, int16, int32, int64}
. Since the union is the only element ofPredeclaredSignedInteger
, that is also the type set ofPredeclaredSignedInteger
. This constraint can be satisfied by any of those five types.Here is an example using approximation elements:
The type set of this constraint is the set of all types whose underlying type is one of
int
,int8
,int16
,int32
, orint64
.Any of those types will satisfy this constraint. This is the equivalent of the notation used in the generics proposal
The use of explicit approximation elements clarifies when we are matching on underlying types, the use of
|
instead of,
emphasizes that this is a union of elements, and thetype
keyword can be omitted by permitting constraints to embed non-interface elements.The purpose of introducing type lists in the generics proposal was to specify the operations available to type parameters in parameterized functions. This is easy to define based on the idea of type sets. Given a type parameter
P
with a constraintC
, a parameterized function is permitted to use an operation with a value of typeP
if the operation is permitted for every member of the type set ofC
.That is the complete proposal: a conceptual change to use type sets, and three new syntax changes. We will now mention some details and ramifications.
Approximation elements
The new
~T
syntax will be the first use of~
as a token in Go.Since
~T
means the set of all types whose underlying type isT
, it will be an error to use~T
with a typeT
whose underlying type is not itself. Types whose underlying types are themselves are:[]byte
orstruct{ f int }
.int
orstring
.We do not permit
~P
whereP
is a type parameter.The type set of
~T
is an infinite set of types.The
~
will bind more tightly than|
.~T1 | T2
means(~T1) | (T2)
, not~(T1 | T2)
(note that~(T1 | T2)
is not syntactically valid)..The new syntax is
Embedding constraints
A constraint can embed another constraint. Union elements can include constraints.
Interface types in union constraint elements
The type set of a union element is the union of the type sets of all elements in the union. For most types
T
the type set ofT
is simplyT
itself. For interface types (and approximation elements), however, this is not the case.The type set of an interface type that does not embed a non-interface element is the set of all types that implement the interface, including the interface type itself. Using such an interface type in a union element will add that type set to the union. For example:
The type set of
Stringish
will be the typestring
and all types that implementfmt.Stringer
. Any of those types (includingfmt.Stringer
itself) will be permitted as a type argument for this constraint. No operations will be permitted for a value of a type parameter that usesStringish
as a constraint (other than operations supported by all types). This is becausefmt.Stringer
is in the type set ofStringish
, andfmt.Stringer
, an interface type, does not support any type-specific operations. The operations permitted byStringish
are those operations supported by all the types in the type set, includingfmt.Stringer
, so in this case there are no operations other than those supported by all types. A parameterized function that uses this constraint will have to use type assertions or reflection in order to use the values. Still, this may be useful in some cases for stronger static type checking. The main point is that it follows directly from the definition of type sets and constraint satisfaction.Combining embedded non-interfaces with methods
A constraint can embed a constraint element and also list methods.
The rules for type sets define what this means. The type set of the union element is the set of all types whose underlying type is one of the predeclared signed integer types. The type set of
String() string
is the set of all types that declare that method. The type set ofStringableSignedInteger
is the intersection of those two type sets. The result is the set of all types whose underlying type is one of the predeclared signed integer types and that declare the methodString() string
. A function that uses a parameterized typeP
that usesStringableSignedInteger
as a constraint may use the operations permitted for any integer type (+
,*
, and so forth) on a value of typeP
. It may also call theString
method on a value of typeP
to get back astring
.Empty type sets
It is possible to write a constraint with an empty type set. There is no type argument that will satisfy such a constraint.
The compiler should give an error whenever it detects such an unsatisfiable constraint. However, in general a compiler may not be able to detect all such cases.It is not feasible to detect all such cases, though they can't be used with any type argument. It may be appropriate to have vet give an error for cases that it can detect.Method sets of constraint elements
Much as the type set of an interface type is the intersection of the type sets of the elements of the interface, the method set of an interface type can be defined as the union of the method sets of the elements of the interface. In most cases, an embedded element will have no methods, and as such will not contribute any methods to the interface type. That said, for completeness, we'll note that the method set of
~T
is the method set ofT
. The method set of a union element is the intersection of the method sets of the elements of the union. These rules are implied by the definition of type sets, but they are not needed for understanding the behavior of constraints.Possible future step: permitting constraints as ordinary interface types
We have proposed that constraints can embed some additional elements. With this proposal, any interface type that embeds anything other than an interface type can only be used as a constraint or as an embedded element in another constraint. A natural next step would be to permit using interface types that embed any type, or that embed these new elements, as an ordinary type, not just as a constraint.
We are not proposing that today. But the rules for type sets and methods set above describe how they would behave.
Any type that is an element of the type set could be assigned to such an interface type. A value of such an interface type would permit calling any member of the corresponding method set.
This would permit a version of what other languages call sum types or union types. It would be a Go interface type to which only specific types could be assigned. Such an interface type could still take the value
nil
, of course, so it would not be quite the same as a typical sum type.In any case, this is something to consider in a future proposal, not this one.
The text was updated successfully, but these errors were encountered: