-
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 new constraint kind satisfied by types that support == (including interface types) #52531
Comments
Ah, now I see what is the problem. It is still allowed to use == on any types, for nil comparison, so the subset relationship would not hold if we change comparable. In that case, indeed it is better to keep comparable as it is, and have a different name for this. We need a better name than HasComparisonOperators, though. Perhaps |
That's a great point that you raise. Regarding the name, I have no idea. Doesn't commensurable mean "measurable" though? |
A generic map, which allows both a) O(1) access to elements and b) allows iteration in sorted key order. For example, it might use a binary tree as storage, alongside a I think as soon as you require to be able to mix these, the entire case for this seems to fall down. If we need to be able to combine them, whatever solution to whatever problem you come up to do that, can just as well be applied to what we call "constraint interfaces" (of which
i.e. the sole advantage of your On a general level, having two different notions for a constraint for equality operators, which behaves subtly differently, seems like a very confusing idea. It certainly violates the goals of orthogonal language features. |
Most types are not comparable to the predeclared identifier |
Sorry, I'm not sure I understand. What does it change for the map key types? What additional constraint should be used to constrain the type parameter of such generic map?
Not sure I understand either. The issue of being able to combine them is a bit orthogonal as it would only appear in generic code. But it does mean creating some kind of syntax (for example similar to boolean connectives) and I'm not so sure that we want that. But the arguments about typeset computations would remain.
I think that this is a major point, yes. It also leaves us with the possibility of reifying any interface constraint. At least, it does not preclude it from the get-go.
As I've said, the current |
Yes.
Yes, that's the situation where are in, currently. Though I'm not sure "reifiable" is the right word. |
I am overwhelmingly opposed to adding a new identifier which means very close to the same thing as
I find this claim irrelevant, if not dubious. We check whether a type satisfies a constraint by verifying that the type satisfies all of the constraint's intersection elements. Aside from methods which are listed explicitly, we find what operations are in the operation set of the constraint by the intersection of the types listed in its union terms. Neither of these things depend on transitivity. I don't think there is any context in an implementation of Go where it is useful to compute an entire type set.
What if I want to constrain a type parameter to any comparable type with a method |
It does not create much to learn, conceptually. I would also add that most beginners would probably not start creating generic code and what I propose would almost only affect generic code. So I think that this argument might be a bit overblown.
All this is possible by virtue of typesets being transitive sets. Transitivity in that case is stable wrt set union and set intersection. The main issue with interface types being included in typesets is that I think it messes with the accuracy of the typeset computation. It can be proven easily (I explained why in another issue). As far as computing an entire typeset is concerned, that probably depends on whether we are talking extensionally or intensionally.
If you happen to read the discussion with @Merovius, who gave a great example, we could decide to add some syntax to be able to combine both kinds of constraint. A quick idea could be to put the list of constraints between parens, comma-delimited package main
import "fmt"
type Map[T (HasComparisonOperators, interface{String()}), V any] map[T]V
func main() {
fmt.Println(Map[fmt.Stringer,any]{})
} Or we could lose the parens, use the ampersand symbol Obviously, I know that for interface constraints, the way to do it is via embedding. In practice, I don't expect things to get too confusing. But that's my impression and I would defer to the potential implementers here. Out of sheer curiosity, do you have any example of such constraint that would occur in real code? |
I have to agree with earlier comments that given func F1[T comparable](a, b T) bool
func F2[T HasComparisonOperators](a, b T) bool then the difference between Also it's hard to define the idea of a non-interface constraint, because of one generic function instantiating another. func F1[T comparable](a, b T) bool { return a == b }
func F2[T HasComparisonOperator](a, b T) bool {
F1(a, b) // OK?
F1[T](a, b) // OK?
F1[any](a, b) // not OK
} |
There is some overlap. Maybe it is because I've been thinking about it lately but the difference between the two constraints doesn't strike me as something too difficult to understand.
I think that none of those cases should compile actually because the bounds for the type parameter edit: to try and be clearer, |
Some pointers from someone who does little to no proposal work: Equality operators in programming languages are hard. Exceedingly hard. I claim that it's because we (Programming Language Designers / Type Theorists / Computer Scientists) don't really have a good grasp of what equality means. We want some kind of sameness, but judging a bit:
Interface equalityThe key point about interface equality is that it is heterogenous. We can compare interfaces of different type. Whereas comparison on ground types are homogenous in that the type must be the same type on both sides of the operator. At a first glance, It's one of those places where we---suddenly---take a giant leap of faith, bundle another kind of equality onto our Some part of me thinks we are happy with Frankenstein Monsters. We even allow the Frankenstein Monster to This is the basis for my claim that we don't really understand sameness too well. Problem StatementHeterogenous-Equal (HEQ) can be nice to have, but I'd like to see a more fleshed out problem statement, as I think it goes for a solution a bit too early. In particular, try to find examples where the current situation is inadequate, and where the examples point in "different directions" in the sense that they shouldn't be reiterations of the same idea, posed in new ways. Then, when a solution is proposed ("Add HEQ"), it can be shown that it's "right" by the fact that it covers all of the examples. It also gives rise to alternatives. Maybe some of the examples can be solved in different ways. Maybe there's a different equality definition which fits better with the goals of Go. Perhaps a generalization is possible, etc. To squeeze out examples, find issues or places in existing code where the proposal will help. This informs the cost/benefit analysis: core functionality imposes a cost in implementation, but that's just a constant factor. Much more important is that the cost scales linearly with users of the language since they all have to learn about the functionality. In turn, the benefit needs to be more than just an abstract theory. There must be some real value, and it also has to have a certain weight. For instance, I like the analysis Russ Cox did for Opinion statementMy hunch/intuition is that it's too early to tell. People have only had generics in the language for a very short time span. Go 1.0 came out in March 2012. Go 1.18 in March 2022. That's 120 months, we've had Go with a stability guarantee. But only 1 month we've had generics. So I'm going to claim we don't really understand how generics will settle in the language and what is possible. This is why I think one should round up some examples of what's not currently possible for a while. It makes a possible proposal far stronger because it can claim to solve a number of these pain points people have, where nobody found a good way to work around them. Another hunch: a subtyping relation should always pose the following questions:
Because it is very likely to show up at some point. Discharging it might be possible, but my spider sense always tingles if subtyping is mentioned without these two items. The usual question to get the ball rolling is: "how does thing thing work when we start passing functions around?" and "we want to pass a generic parameter in a covariant and contravariant position. How?" Go usually gets around this question because interface embedding isn't a subtype relation. |
Indeed, I should have added more context. This proposal was initiated as a kind of counter proposal so I forgot to add the background details. I will link these cases in the problem statement. It will be clearer.
I think I might have an answer to the first question. I think that we have a subsumption rule if we restrict ourselves to the domain of interface types. However type constructors in Go are invariant so co/contra/variance does not matter too much in practice. Regarding the second question, I am not too sure of what you mean by dealing with bounded polymorphism. (HEQ is nicely put! I wasn't really thinking about it this way) |
I think even though variance doesn't matter directly, it still guides questions in that you have to ask the question of what happens when you call one from the other. i.e. what happens when you call I think that's relevant here, because I think it exposes the main issue with this proposal. Either a) both directions work, in which case they really describes the same set of types and we don't need two names, or b) at least one direction doesn't work, in which case it seems likely that you run into situations where it's not clear which to use - or one of them is just always better, in which case you also don't need two names. I don't understand well enough what you are proposing to actually apply this argument. But it might help you to make your case or understand the concerns people have. |
This still would not really be a variance issue but a consequence of the subsumption rule. Where you are right is that interface implementation and constraint satisfaction look really similar and it does look similar to a variance issue.
It should be clear because it uses the same kind of set inclusion check we use for interface constraints. It's really just an extension of the same logic, just including interface types. A bit like a superset of typesets.
I hope I was able to make things clearer. The main point is that although this proposal adds the concept of non-interface constraint, it allows to simplify the relation between interface implementation and interface constraint satisfaction. Remains the issue of naming those constraints. |
@atdiar FTR I still do not understand what exactly the semantics are you have in mind both for how I find your abstract arguments about type set inclusions and constraints/interfaces/constraint kinds/reification impossible to understand. A set of "this piece of code currently does not compile/compiles and does X and would, under this proposal not compile/compile and do Y" is significantly easier to understand and IMO the best practical way to evaluate proposals. As it stands, the only thing I, personally, can go by for evaluating this is "I don't even understand what you are proposing, so it's obviously too confusing to explain to people". |
For an example of how it would work, see @ianlancetaylor 's question #52531 (comment) and my answer #52531 (comment) Instead of using |
See #52614 for another take on this problem. |
@ianlancetaylor Also taking advantage of this to say that, concerning the current proposal, if we used the provision from the backward-compatibility promise, |
Sorry for being glib, but "one is a type set, the other is a set of types" is as close as you can get to "not a practical difference". I've asked a couple of times now, but I'll ask again, if need be: Can you provide some code this allows you to write, which the other proposals don't? Or vice-versa? Until that happens, we're left with trying to interpret this proposal as best as we can. And my interpretation is "it doesn't have any practical differences to either #49587 or #52509, depending on which version of your proposal we look at". |
With the current restrictions on union element interfaces you're right. But if we wanted to lift those restrictions at some point we would then be stuck. type CompString interface{
~string | fmt.Stringer
comparable
} And I know there are limitations now and it is not allowed but we have to take into account that it is the very first iteration. Again, just future-proofing. Edit: now that I think about it again, maybe it is ok. As long as we keep the type set and the set of permissible types different.🤔 |
If I understand you correctly, you are hinting at some hypothetical problems in the future, if we ever lift this restriction:
First, I don't think, currently, we ever can lift that restriction. No matter what happens to Second, I don't think that constraint is any harder to handle than, say type C interface{
~string | fmt.Stringer
M()
} It might be easier, but it certainly is not harder. Neither in spec, nor in implementation. Unless you can make a very good case for why that would make difficulties, I'm certain that is not a problem. Third, there isn't actually a difference between those proposals in how they would handle this. They only differ in what types fulfill the I'm not just handwaving here. As you know, I discovered the difficulty of calculating type sets in the first place. And I spent a lot of time thinking about it and writing proofs. If I would see any issue related to that, I'd be the first to speak up. It's just not relevant here. And TBH, I find it a bit frustrating to see a problem which I spent quite a bit of time and care laying out invoked in such a haphazardly and unrelated manner. Feel free to write a proof showing that I'm wrong and these problems might exist. Until then, I really feel you should drop that claim. |
Yes, that would be the restriction in question. But my example is a little bit of a red herring. type CompStringer interface{
fmt.Stringer
comparable
} And how it would compare to fmt.Stringer So the answer seems to be that making interfaces implement Contrast this with your claim that interface types would be included in type sets. Maybe it is not obvious for anybody... |
You are, again, moving into abstract arguments about type sets and inclusions, instead of talking about actual Go programs and how your proposal would change their behavior. I don't see the point in arguing anymore, it just adds more noise and goes in circles. As far as I'm concerned, a language change proposal - in particular one, which tries to solve our current problems with |
I think people (especially beginners) will have to understand those concept of set inclusion to reason about the below: func F1[T fmt.Stringer](a, b T) bool { return Eq(a,b) // ok? don't think so... }
func Eq[T comparable](a, b T) bool {
return a == b
} Even if fmt.Stringer implements That's why what I was proposing would instead simply allow to adjust the set of permissible type arguments for a type parameter. The current proposal as written would allow: func F1[T fmt.Stringer & comparable](a, b T) bool { return Eq(a,b) // ok }
func Eq[T comparable](a, b T) bool {
return a == b
} where The difference with other proposals is that this set would be formed from a conjunction of constraints, one of which would not be an interface. So it would prevent the creation of new interfaces that could not be turned into types. Compared to being able to define this: type CompStringer interface{
fmt.Stringer
comparable
} where it is not obvious to me which set of permissible types it denotes. Does Sorry for being confused if those are truly the same proposals but I think not. |
Not under any of the proposals currently discussed, I think.
Comparing that to type ComparableStringer interface {
fmt.Stringer
comparable
}
func F1[T ComparableStringer](a, b T) bool { return Eq(a,b) // ok }
func Eq[T comparable](a, b T) bool {
return a == b
} Which is allowed under the current rules, there doesn't seem to be any difference. Except that you allow |
My point, that I kept repeating, is that it's different because this proposal does not allow the creation of interfaces that could not be turned into types. That's why we were disagreeing on the names for the Hope it's clearer now. |
I don't think your proposal is different in that way either. If we adopt #52509, everything that speaks in favor of making |
I don't get it. How would you embed in an interface something that is not an interface? If you keep #52209 does not make this distinction. Now what is possible is to have an interface denoting the set of types that allow panic-free comparison (which correspond to the current implementation of These are truly different proposals. |
Okay. I believe it is a bad idea to introduce a new system to express constraints, which needs to carry all the same complexities to achieve its expressiveness (i.e. you still need the same ways to be able to express conjunctions and disjunctions and methods and type sets), but behaves subtly different than an existing one. To me, that seems an obvious non-starter, so I was trying to reconcile it into something workable, by extracting the core ideas. Apologies for the noise. |
Well, no worries. At least, you hopefully understand exactly what is being proposed. And I am not the only one thinking that it could be done. Anyway, happy it's clear now. Then again, I would be happy if someone has an even better solution. |
Yes, the "something else" from that comment is what we call type elements today. It's not something on top of what we call interfaces today, but it's something on top of what we called interfaces then. You should pay attention to the historical context that comment was made in. I assume the "active work" he is referencing is #45346 (note the dates). The non-starter is adding a different constraint system to what we have today. |
Not necessarily..
(emphasis mine) Where does it say that it has to be type elements? That's your interpretation. |
Implementing an interface does not (should not?) necessarily mean satisfying the constraint it denotes:
From this, we can wonder if:
Note that this second point does not mean that constraints cannot be turned into types. Just that they might not all be turned into interface types. Mostly because interfaces determine set membership differently. In a nutshell, this proposal attempts to:
In doing so, we also retain one current interesting property: assignability test being true means that we have statically determined that operations such as a comparison will not panic. |
Actually, I think it could be simplified: There are essentially two ways that type sets can be defined:
Then the difference between interface implementation and constraint satisfaction is that:
Even if the type parameter value is an interface type, we do not rely on its type set inclusion but on whether the interface belongs to the type set. In terms of theory, I believe that it means that a type parameter is a variable urelement. As such, there shouldn't even be a need to introduce much change.
I think it is leading me to the same result as in #52509 (comment) So rescinding what I wrote earlier, we can perhaps generalize slightly the concept of a basic interface to accomodate for The only thing is to be careful about any predicates that are in relation to each other, when checking type set inclusion, I guess. Most often, there shouldn't be any issue. Might involve De Morgans laws in some other cases (e.g. conflicting methods?). |
Yes well, might as well close this issue. Might not be necessary but I will open a new one just for the sake of clarity. |
Problem Statement
Currently, interface types and composites of interface types do not implement the
comparable
constraint interface.Yet, the use of the comparison operators (
==
and!=
) is allowed for interface types (and composites) in non-generic code.We would like to be able to use those operators in generic code as well.
It should allow us to use interfaces such as
reflect.Type
orast.Node
in generic Set or Map datastructures by default.Proposed Solution
We need a new constraint that is not an interface, for reasons we explain below.
This might well be a standalone unless there are other interesting operations available to interface types, that are shared only by some but not all members of the typesets they determine.
A quick, non-committing, idea would be to make it a predicate constraint e.g.
HasComparisonOperators
(too lenghty a name!!)It should solve issue #52474 and permit the definition of generic Sets and Maps accepting interface types as type parameter values.
Explanation
For a summary of the issues at stake, skip to: #52624 (comment)
Go1.18 introduced constrained parametric polymorphism in the language.
The type constraints that can be written in 1.18 are under the form of interfaces.
Interfaces in Go denote set of types called type sets that can be defined as the set of non-interface types that implement said interface. Type set inclusion and interface implementation are in practice linked in an equivalence relation.
These type sets should mirror the subtyping relation of interfaces by being transitive sets meaning that:
Given three interfaces
A
,B
,C
such thatA
implementsB
andB
implementsC
,we should be able to infer that
typesetOf(A) ⊂ typesetOf(C)
(equivalent toA
implementsC
).This was always the case for pre-1.18 Go when we only had Basic interfaces. This should extend quite naturally to all interfaces in Go 1.18
We posit that this property of typesets (transitivity), along with the equivalence relation between interface implementation and typeset inclusion , is very advantageous if not necessary for their accurate computation as well as the computation of the operation set and/or method sets, available to the members of typesets.
We also claim that the current issue of comparability/availability of comparison operators stems from the fact that it does not follow the subtyping relation.
If we take the simple example of the
any
interface, function types implementany
as they are members of its typeset.However they do not possess the comparison operators. Yet
any
, as an interface type, can still use the comparison operators.It shows that an interface type having access to the comparison operators cannot be inferred from its typeset. The operations of interface types are independent from the operation set defined by the respective typesets.
So what now?
We need a new way of defining the permissible set of types that defines a type constraint.
This new kind of set should not be defined by an interface (so that it does not get embedded or it might interfere with the typeset computations). Such a set will be able to directly include interface types (and composites). Therefore, it won't have a type set.
As shown above, inducing operations of interface types from their type set is insufficient in the case of comparability. It only works for the current implementation of
comparable
and its embeddings.Do we still need the
comparable
interface constraint or #51338 ?We may not need it as much anymore but it may not hurt to have it either (see #49587 ).
Besides,
comparable
should belong to the set denoted byHasComparisonOperators
by definition.What about the interactions between two kinds of constraint?
Since
HasComparisonOperators
may only be used in generic code to constrain generic map key types for example, that's where we should place our focus.An example of interaction is the case of nested functions where the inner function uses
HasComparisonOperators
and the outer function uses interfaces as constraints for a same type parameter.It's presumed that
HasComparisonOperators
should apply further bounded quantification to the typesets accepted by the outer function.Said more simply, the set of permissible types for the outer function has to be a subset of the set defined by
HasComparisonOperators
.Shouldn't we be able to combine the two kinds of constraint?
A priori, there would be no need for it. So no need to create extra syntax.We don't think that there are many cases where one wants a subset of all types satisfying
HasComparisonOperators
since, typically, the sole constraint on map key types is to have the comparison operators.Maybe someone can come up with a rebuttal use-case though.
[edit] @Merovius found a rebuttal-case.
We need to find another way equivalent to interface embedding to combine constraints.
That should be easy enough.
A type parameter definition could accept a list of constraints. For instance:
That could look like this:
Why not just change
comparable
?[edit] we could by using the provision from the backward-compatibility promise.
"comparable" is a good name. But for the aforementioned reasons that changing
comparable
would interfere with typeset computations, we think thatcomparable
should retain its current semantics.Creating a new kind of constraint might add a bit more complexity to the backend. But the advantage is that it would not interfere with typeset computations, leaving us with some leeway to use all interfaces as types later.
This could be a crucial point in the discussion about sum/union types for instance.
The alternative* could more easily preclude us from entertaining using interfaces as union types (#19412), should we see a need for it (because typeset calculation would not be precise). It might be better to leave the design space open, just in case.
(*) the alternative as proposed by some would be to change the
comparable
interface. It would still be an interface constraint :As such it would be embeddable but reifying it into a type would not be possible.
It would create two subcategories of interface constraints : those that embed
comparable
and would not be reifiable into types and the rest that would be reifiable.Other naming ideas?
weakcomparable
comparable-maypanic
comparable-unsafe
We could also change the implementation of
comparable
so that it is equivalent to ourHasComparisonOperators
In which case we would take advantage of the provision in the backward-compatibility promise.
Changes to the Specification
Just a few notes, leaving that part to the more experienced.
C
would be said to be satisfied by a typeT
if(f?)T
belongs to the set of permissible type arguments defined byC
.comparable
HasComparisonOperators
or be more restrictive by using the interface constraintcomparable
. The choice would be left to the programmer.References
Alain Frisch, Giuseppe Castagna, Véronique Benzaken (2008) Semantic subtyping: Dealing set-theoretically with function, union, intersection, and negation types
G. Castagna, T. Petrucciani, and K. Nguyen (2016) Set-theoretic types for polymorphic variants
Transitive set. Wikipedia
Jech, Thomas J., et al. Set theory. Vol. 14. Berlin: Springer, 2003.
The text was updated successfully, but these errors were encountered: