-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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: built-in functions for generic type constraints #44235
Comments
When are |
|
So are they a special kind of function that are not called and cannot be called? What is the advantage of introducing a new special kind of function in Go (and I don't mean two special functions but a new kind of function), compared to introducing two new keywords? |
It seems to me that calling type SignedInt any {
int; int8; int16; int32; int64
} If you want to add an interface as well, you can just add it as type parameter: // Type list in {} is optional.
type GenericStringer any[Stringer] Then your example becomes the following: func Print2[T1, T2 any](s1 []T1, s2 []T2)
func ConcatTo[S any[Stringer], P any[Plusser]](s []S, p []P) []string
type Ordered any {
int; int8; int16; int32; int64
uint; uint8; uint16; uint32; uint64; uintptr
float32; float64
string
}
type ComparableHasher comparable[interface {
Hash() uintptr
}] The downside to this particular syntax is that it requires All of that being said, this proposal feels somewhat unnecessary to me, though. It still doesn't solve the primary problem of type lists. It just fixes the dichotomy caused by the difference between normal interfaces and interfaces with type lists in terms of non-generic usage. The main problem that needs fixing, in my opinion, is the lack of usability of type list interfaces with user-defined types due to the lack of operator overloading and/or operator methods, resulting in the requirement of two implementations for anything that uses operators and also wants to support user-defined types. That's the primary reason that type list interfaces exist in the first place and the main reason that they can't be used as normal interfaces, so I think that fixing that will probably automatically fix this issue, too. |
CC @griesemer |
Am I correct in thinking that this is a purely syntactic change? The semantics are the same as in the current generics proposal, it's just that type constraints are written differently? As @DeedleFake suggests, in this proposal |
FWIW, If
If
So I don't actually think that any change is needed to |
The novel part of this proposal seems to be the However, that form is less expressive than type-lists: type-lists allow both disjunction (using comma-separated entries as in It isn't obvious to me whether the loss of the “intersection” operation is significant, but I suspect that it may be. |
I don't think the intersection property of embedding type lists will ever be useful in practice. It falls out of the design, it's not a goal or even a useful feature. |
To me, type lists are orthogonal to that dichotomy. It is really between interface types and type constraints. I think it is a mistake, especially for explaining the idea to programmers new to Go, to make those into one concept. Even without type lists, they're incompatible things. This proposal is not about solving any problems related to type lists.
That is the goal, yes. Any behavior that is possible with the current draft should be possible with this proposal. Apparently, I completely missed the section on type lists in embedded constraints, however. My assumption was that it would be illegal to embed an interface when it would cause there to be more than one type list in the interface definition. So, this proposal has no way to express intersection of type lists. As you and @bcmills discussed, I can't think of a way in which that is a useful operation.
I agree with @DeedleFake that More precisely, I believe these would be metatype constructors. I want to avoid talking about metatypes in the proposal; even if accurate, I'm not trying to suggest anything about adding metatype programming to Go. However, unless my definition of "type constructor" is incorrect, these are precisely what generics adds.
More precisely, |
New keywords would violate the Go 1 compatibility promise. New predeclared identifiers do not. |
I think it is a mistake to introduce a new language concept such as a constraint just to separate the different use cases for interfaces, at least if we ignore type lists. And ignoring type lists for a moment (as you state "This proposal is not about solving any problems related to type lists."), interfaces seem exactly the correct mechanism to describe constraints. From the perspective of a caller of a generic function, a type argument satisfies a constraint if it implements the constraint interface. From the perspective inside of a generic function, the type parameter constraint is exactly the interface of the type parameter. These concepts are well understood. But here's a concrete technical argument as to why introducing a new concept such as constraint seems unwise. For example, let's say we've got a simple generic type List[T any] struct {
next *List[T]
elem T
} It makes perfect sense to instantiate such a generic type with its own constraint: type PolymorphicList List[any] Now we've gone from a generic (parameter polymorphic) list with a fixed element type to a non-generic list with a polymorphic element type. Here the constraint is just Type lists complicate matters. The final word has not been spoken yet; and we may well end up with some additional mechanism besides just interfaces. |
Just to be clear, there are a LOT of people concerned about type lists in interfaces creating a sort of "interface that cannot be used in most ways a normal interface can". Besides people who are completely opposed to generics in the first place, I think people concerned about these "special case interfaces" are the next biggest opposition to the current (accepted) version of the generics proposal. If the sum-types proposal is unable to re-use the type list semantics, then we would likely be stuck with the implied "(except if the interface contains a type list)" in most discussions of interfaces when not used as constraints, and this probably needs to not just be implied but explicitly stated in some beginner oriented materials, which is really unfortunate. This proposal is just one of likely many attempts to find a way around this unfortunate limitation. A separate constraint concept is one possible solution. My own reservations about generics are: a) this second class interfaces issue and b) the extra type-parameter that gets auto-inferred approach to solving the only the pointer implements desired interface concern. (With the latter, I'm concerned that if a cleaner solution is added later, existing generics in the standard library would need to still use the old approach indefinitely, since some callers may have explicitly specified the second type argument since they can). |
@KevinCathcart Thanks, Kevin. Let me assure you that we are very aware of the problems with type lists in interfaces and that such "interfaces cannot be used in most ways a normal interface can". We (Go team) are actively working on it, with @bcmills involved in the discussion, and we have some promising ideas that very much are taking into account many of the observations made by the community. But I still hope that ordinary interfaces (no type lists) can remain usable as constraints directly. The problem are type lists, and their inclusion into ordinary interfaces which muddles the latter. I updated (edited) my comment to be clearer about that, but perhaps not clear enough. I do think it would be fairly sad if we could not instantiate a generic function or type with its own constraint if that constraint is an ordinary Go interface. This proposal is recognizing that type lists are a problem as well and addresses this by introducing a new mechanism (constraints) using built-in functions. What I have reservations with is that even if the constraint is an ordinary interface, one has to wrap it first with a built-in function call. Also, using a built-in call rather than syntax seems a bit of a deviation from how we construct types (and type-like things such as constraints) in Go. This proposal could avoid the former by simply allowing ordinary interfaces to be used as constraints as well. Thinking out loud, one way of looking at it is that "to satisfy a constraint" doesn't necessarily have to mean "to implement an interface" as is the case in the current generics proposal. It could mean "to implement the interface if the constraint is an interface", and "to do <something else> if the constraint is <something else>". And then a constraint could be two different things, or a combination of two different things (such as a type list and an interface); very much like we don't just have one kind of type, we have many kinds of types. And then ordinary interfaces are not "polluted" by type lists, they remain exactly as they always were, but they now can also be used as type constraints. And in addition we have another mechanism, call it type lists, which are different from interfaces, and which may also be used as constraints. And then we need to figure out how to tie it all together nicely, both syntactically and semantically. |
I received feedback elsewhere that allowing As you point out, though, type constraints as an independent concept would not be orthogonal to interfaces. I still believe that interfaces as constraints makes them no longer unitary, to continue the linear algebra terminology. However, considering my original motivation for proposing this – that being the fact that I expect to answer many questions about why people's generic code doesn't work how interfaces lead them to expect – I think it would be enough of a solution to be able to instantiate a generic type or function with its own constraint in every case. That would at least make it easier to illustrate how type parameters and interfaces are different. So, contrary to what I said before, this proposal may indeed defer to a better approach to type lists (or perhaps sum types). In particular, I agree with everything in the last paragraph of your previous comment. |
Closing now that #45346 is accepted. I don't imagine there is much interest in changing the syntax anymore. |
Now that #43651 has been accepted, I propose changing the spelling of type constraints to use the new names
any
andcomparable
as built-in functions that produce type constraints. Accompanying these built-in functions in this proposal is the addition of type constraints as a fully independent concept in the language and type system, rather than reusing interface types. Furthermore, the type list syntax proposed in the type parameters draft would be removed.Motivation
This proposal addresses concerns from @bcmills (here) and I (here) raised in #43651, as well as many others who discussed type lists at length.
Keeping type constraints and interfaces distinct removes many caveats and special cases, simplifying the mental model required to understand generics. In the type parameters draft, interface types may also serve as type constraints. This makes the meanings of "interface" and "type" much more intricate, or at least requires a variety of caveats to distinguish the two different uses. By constructing type constraints from interfaces, the distinction becomes much clearer.
Giving an explicit name to type constraints in code as well as in the specification makes it much easier for experienced Go programmers to learn generics and for new programmers to learn Go with generics. Arguably, interfaces are already the subtlest concept in the language. The type parameters draft further increases their subtlety by creating situations where they are no longer types, at least in the current sense in the context of Go. The concept of interfaces in Go almost becomes reminiscent of the many meanings of
static
in C++. By separating type constraints from interface types in code, learners can acquire one concept at a time.It is possibly clearer where to modify the Go specification to add built-in functions than to add type lists to interface types. Admittedly, this benefits very few people. Still, an important aspect of Go is that its specification is easy to read. Generics will add a significant amount of text to that document, so it is crucial that these additions be as straightforward and readable as possible. The spec already contains a section on built-in functions, which begins:
These paragraphs would require only minimal modifications to describe the properties of the new built-in functions here proposed.
Lastly, this proposal reduces the number of special cases added with type parameters, at least insofar as every existing built-in function already being some special case. The type parameters draft introduces a special predeclared identifier
any
that is defined as "equivalent tointerface{}
," but with the unusual exception that it can be used only as a type constraint. Similarly,comparable
is an interface containing every comparable type in a type list, which is not normally possible to express in Go code. By using these names for built-in functions instead, their definitions are more consistent with the language, and their special properties follow from the usual traits of built-in functions.Proposal
Let
I
denote some interface type andT1
,T2
, ...,Tn
be a non-empty list of types. Then theany
built-in function would accept the following forms:¹any()
constructs a type constraint with which any type may unify. (This matches the current draft behavior ofany
.)any(I)
constructs a type constraint requiring a type to implementI
to unify with the constraint. (This matches the current draft behavior of supplying an interface without a type list as a constraint.)any(I, T1, T2, ..., Tn)
constructs a type constraint requiring a type to implementI
and to either be a member of the listT1
, ...,Tn
or to be a defined type whose underlying type is a member of the same list to unify with the constraint. (An example spelling might beany(interface{}, float32, float64)
. This matches the current draft behavior of type lists.)The
comparable
built-in function accepts the same forms asany
and adds the requirement that a type must be comparable to unify with the constraint it produces. It is an error during type checking if any type in the type list given tocomparable
after the first argument is not comparable.The result of all forms of
any
andcomparable
is a type constraint. Type constraints are new citizens of the type system that may appear in exactly two contexts:type C any(...)
ortype C = any(...)
(orcomparable
).Defined type constraints may additionally be parameterized as if they were types.³ So, for example, if
I
isthen
type C[B any()] any(I[B])
defines a constraint requiring a type to implementI[B]
.Examples
Comparing declarations under this proposal to those of the type parameters draft document:
become
And the more involved, mutually dependent parameterized type definition in the draft:
becomes
For constraints that previously were only
any
orcomparable
, the only change is the addition of parentheses. On the other hand, for constraints containing only type lists, this proposal requires the addition ofinterface{}
.⁴ So, some generic declarations become more verbose, and others stay close to the same length or are unchanged.¹ Another form for
any
andcomparable
which might be desirable would beany(I, C...)
to produce a constraint with the union of the method sets ofI
and the interface used to constructC
and that reuses the type list ofC
. I generally find nested constraints in any proposal unintuitive, so I do not propose that at this time.² Type constraints are not types, in the sense that there are no values of type
any(...)
. Still, of the existing declaration keywords,type
seems the most appropriate to define a type constraint.³ A consideration is whether
any
andcomparable
should accept type parameters directly, as inany[K any()](I[K])
. I think this makes declarations excessively noisy.⁴ I considered proposing untyped nil in place of
interface{}
as the first argument when creating a type list, but I'm not confident that's a sensible, intelligible decision.Experienced.
C, Java, Python, C++, various assembly languages
This change will make it significantly easier to learn generics in Go, because the existing concept of interfaces are no longer gaining a new, subtle meaning.
To my knowledge, only syntactic approaches have been proposed for type constraints. This proposal instead adds new identifiers to the universe scope.
This proposal helps people who teach Go, because we no longer have to explain many new caveats on interfaces, which are already one of the subtlest concepts in Go. Transitively, it helps people to learn Go, whether those people are new to Go or new to programming, because distinct concepts remain distinct.
Yes. The only changes are the addition of predeclared identifiers in the universe scope.
I am given to understand that some work has already been put into implementing the type parameters draft, including the current meanings of
any
andcomparable
, the current syntax for type lists, and the idea of interface types as constraints. This might reverse some of that work. Additionally, this proposal may or may not require greater separation between interface types and type constraints in cmd/compile. Otherwise, there should be no difference between this proposal and the current draft for external tools, compile-time costs, or run-time costs.The text was updated successfully, but these errors were encountered: