-
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
spec: allow basic interface types to instantiate comparable type parameters #56548
Comments
This seems like a small and clever rule with intentionally broad consequences. I'm not sure why this approach is preferable to, in particular, #52509 (comment) by @Merovius. Adding the "satisfies" relation seems to be helpful for the go/types API, but we can do that and also have a list of small explicit rules rather than a very big implicit one. Perhaps that's just my reading. |
The approach suggested by @Merovius changes the meaning of type sets to include interfaces, which seems like a pretty big change. The end result may still be the same, but I haven't tried to prove that in any way. If we want an interface, say |
Since after implementing this comparable is equivalent to any since any will satisfy the comparable constraint, I don't understand why comparable is needed. What can we model with a comparable that will include any that we can't model with any alone? There may be advantages when stenciling out the actual usages, but type safety is not improved over just using any. Also when following this proposal one again cannot model comparable types without special support for equality that comparable enables right now. I don't see a plan for regaining this capability in in the proposal. |
I do not understand what you mean by your second paragraph. Can you provide an example of the capability you're missing? Finally, you may want to try and play with the actual implementation of this feature which is available to you at tip (see the Implementation section of the proposal). Perhaps that helps clarifying the impact this would have on your code. |
Thanks for sharing this proposal. It broadly makes sense to me and I believe it will allow me to write the programs I described wanting to write in some of the previous proposal discussions (which I will not repeat here). I do have a clarifying question, though: The proposed text constrains The following section of the specification defines "Embedded interfaces" without describing the interaction between "Basic interface" and "Embedded interface". Is it true to say that an interface which embeds a basic interface is itself a basic interface as long as it otherwise meets the definition of a basic interface? Concretely, is type A interface {
Foo()
}
type B interface {
A
Bar()
} |
Yes,
In other words, we mean all interfaces that can be written in the form |
Ahh yes, thank you for clarifying that. I clumsily read "can be defined entirely by" as "are defined entirely by". |
Thank you for the detailed proposal. To be honest, I've found some of the explanations a bit hard to follow personally (which might be from my own limitations).
(5) is about struct types whose fields may be interfaces. https://go.dev/play/p/JkAcqvXlirF That however does not mean that these types would implement a potential future Following this, we would not require a distinction between spec and strictly comparable. That takes care of issue 1 and issue 5 as the distinction would merely be between implementing and satisfying. But now, what about 2,3,4 ? What is even implementing an interface and satisfying a constraint, why does it differ? How to build a simple mental model?That's where I think there has been attempts to establish the (long) list of requirements for each in the spec but I think it shows that the mental model may need to be clarified so that it is made simpler and more obvious.
So as can be seen (in A,D,E), the definition of constraint satisfaction is the basis for a definition of interface implementation instead of the reverse. Also because assignment to an interface value must be true for all satisfying typed values, it is by definition using interface implementation as a requirement. In the case of Constraint statisfaction is what is used by type parameters, so struct{
int
ast.Node
} should still be able to instantiate generic map keys, just like in normal Go code, since it has the comparison operators. Now on the question of whether interface types belong to type sets or not?... it depends. Another question is whether all interface types should satisfy comparable ?As mentioned in this proposal, it might seem less clear for |
I believe the mental model needed here remains very simple: Constraint satisfaction is the same as interface implementation (nothing changes), but we introduce an exception so that Go's (spec-) comparable types can also be used where (strictly) That is all, and that is exactly what users are asking for. The reason we need this is that historically, Go was not statically type-safe with respect to Now, to answer your questions:
struct{
int
ast.Node
} can be used to instantiate a generic map key. Again, that is the goal of this proposal. I very much agree with you that it would all be much simpler if there was only one type of comparability - and we have tried that before (again see #52614 and perhaps others). But users want stronger guarantees. To summarize:
(Regarding your last two questions: a) should interfaces be parts of type sets, and b) should all interfaces satisfy comparable. For a): probably not - but we don't need to answer this question for this proposal. For b) the type I recommend you play with the implementation available at tip. Setting the compiler flag |
@griesemer thanks for the reply. I realize that I misread the examples and made a mistake. As you correctly state, the struct example works. So that's one less issue. Regarding Basically instantiation requires constraint satisfaction. Assignability requires interface implementation (which forces strict comparability) ? |
We still need to be able to talk about the different kinds of comparability. What you are suggesting is that I suppose we could talk about "types that support It seems to me that might be even more confusing. In the spec and documentation it might be clearer to talk about "strictly comparable" types instead and leave established terminology alone. We made a similar terminology change in the past: we used to refer to types declared in a type declaration as "named types". When we introduced alias declarations, the notion of a "named type" didn't make 100% sense anymore, because an alias declaration (e.g. |
Almost but not quite the change I had in mind. Perhaps I'm thinking wrongly though. I would define Quoting the current spec on Comparison operators, The equality operators == and != apply to operands that are comparable. Interface types could be said to be comparable since they can be compared.(just means that interface types satisfy the The difference would simply be that not every comparable type would implement With non-basic interfaces we have the opposite, I think these are nuances that we might want to make clearer. constraint satisfaction seems really different from interface implementation. That might even allow to future proof a potential FutureGo if all constraint interfaces are to be used as types. |
I think the proposal establishes that interface implementation and constraint satisfaction should be different. We can consider this part of the discussion as settled. You are saying we should turn things 180 degrees around: let I don't know how that can work. You state yourself, that "Interface implementation is basically: can every type that fits into one interface also fit into another interface?". That is set inclusion: An interface Similarly, you say that The only potential way I see how this could work is by changing the definition of interface implementation - it could not be based on set inclusion anymore, or the notion of type sets for interfaces would need to be adjusted. If it's possible I'd love to see that. Without a precise definition of the "nuance" you're alluding to it's difficult to assess your idea. There's not enough here to make progress. |
Yes!
Yes, I think that many interfaces are comparable themselves as a type but the types that implement these interfaces are not necessarily comparable as well. In terms of type set inclusion i.e. interface implementation, we can just take the example of
Set inclusion would be set membership of every type in
I think: Type set membership of a type would be constraint satisfaction. |
Let me spell this out: A type
An interface Is that what you are saying? If so, that would require that interfaces (e.g. Is func f[P comparable]() {
f[P] // this should be legal
} I need to think about this some more, but I am going to disengage from this specific conversation for now because it doesn't directly discuss the pros and cons of the proposal at hand (which suggests a very small localized change). It sounds like you are proposing a different idea altogether, which changes how type sets are computed, and that should be discussed elsewhere. Feel free to open a new proposal explaining in detail how your idea would work. Thanks. |
Yes, you got it right, interface types could/would now belong to type sets (as mentioned in #52509) The advantage would be only one notion of comparability, No problem I will amend #53734 to try and specify things more accurately. Thanks. |
Just one last comment: I'm not sure things would be less magic. Right now, an interface is never in a type set. Type sets exactly represent the very real concrete types that can be stored in an interface variable, the types in a type set correspond to the possible dynamic types at run-time. If interfaces become parts of type sets, type sets become an essentially mathematical construct with no direct correspondence to run-time types. Just something to keep in mind. Thanks. |
This proposal has been added to the active column of the proposals project |
Ordinary interface types now satisfy comparable constraints. This change makes the new comparable semantics the default behavior, depending on the Go -lang version. It also renames the flag types2.Config.AltComparableSemantics to types2.Config.OldComparableSemantics and inverts its meaning (or types.Config.oldComparableSemantics respectively). Adjust some existing tests by setting -oldComparableSemantics and add some new tests that verify version-dependent behavior. The compiler flag -oldcomparable may be used to temporarily switch back to the Go 1.18/1.19 behavior should this change cause problems, or to identify that a problem is unrelated to this change. The flag will be removed for Go 1.21. For #52509. For #56548. Change-Id: Ic2b22db9433a8dd81dc1ed9d30835f0395fb7205 Reviewed-on: https://go-review.googlesource.com/c/go/+/453978 Reviewed-by: Robert Findley <rfindley@google.com> Reviewed-by: Robert Griesemer <gri@google.com> Run-TryBot: Robert Griesemer <gri@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Auto-Submit: Robert Griesemer <gri@google.com>
Change https://go.dev/cl/454575 mentions this issue: |
Ordinary interface types now satisfy comparable constraints. This is a fully backward-compatible change: it simply permits additional code to be valid that wasn't valid before. This change makes the new comparable semantics the default behavior, depending on the Go -lang version. It also renames the flag types2.Config.AltComparableSemantics to types2.Config.OldComparableSemantics and inverts its meaning (or types.Config.oldComparableSemantics respectively). Add new predicate Satisfies (matching the predicate Implements but for constraint satisfaction), per the proposal description. Adjust some existing tests by setting -oldComparableSemantics and add some new tests that verify version-dependent behavior. The compiler flag -oldcomparable may be used to temporarily switch back to the Go 1.18/1.19 behavior should this change cause problems, or to identify that a problem is unrelated to this change. The flag will be removed for Go 1.21. For #52509. For #56548. For #57011. Change-Id: I8b3b3d9d492fc24b0693567055f0053ccb5aeb42 Reviewed-on: https://go-review.googlesource.com/c/go/+/454575 TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Robert Griesemer <gri@google.com> Run-TryBot: Robert Griesemer <gri@google.com> Reviewed-by: Robert Findley <rfindley@google.com>
Change https://go.dev/cl/454595 mentions this issue: |
For #54202. For #56548. Change-Id: If2b9e41813c3e1c8d373469a40e1bd0bd5ea2b16 Reviewed-on: https://go-review.googlesource.com/c/go/+/454595 Reviewed-by: Ian Lance Taylor <iant@golang.org> Reviewed-by: Ian Lance Taylor <iant@google.com> TryBot-Bypass: Robert Griesemer <gri@google.com> Reviewed-by: Robert Griesemer <gri@google.com>
Is there an open issue which proposes to use |
I found this issue from #51338, which has been closed. ;D |
@go101 I don't know of one. Personally I think that permitting variables of type |
Change https://go.dev/cl/457095 mentions this issue: |
Change https://go.dev/cl/457437 mentions this issue: |
- Rephrase the notion of "comparability" from a property of values (operands) to a property of types and adjust dependent prose. - Introduce the notion of "strict comparability". - Fix the definitions of comparability for type interfaces and type parameters. - Define the predeclared identifier "comparable" as stricly comparable. These changes address existing problems in the spec as outlined in the section on "Related spec issues" in issue #56548. For #56548. Change-Id: Ibc8c2f36d92857a5134eadc18358624803d3dd21 Reviewed-on: https://go-review.googlesource.com/c/go/+/457095 Reviewed-by: Ian Lance Taylor <iant@google.com> Reviewed-by: Matthew Dempsky <mdempsky@google.com> TryBot-Bypass: Robert Griesemer <gri@google.com> Reviewed-by: Robert Findley <rfindley@google.com> Reviewed-by: Robert Griesemer <gri@google.com>
For #56548. Fixes #57012. Change-Id: I44f850522e52b1811025fb31bcef289da8f8089d Reviewed-on: https://go-review.googlesource.com/c/go/+/457437 TryBot-Bypass: Robert Griesemer <gri@google.com> Reviewed-by: Ian Lance Taylor <iant@google.com> Reviewed-by: Robert Findley <rfindley@google.com> Reviewed-by: Robert Griesemer <gri@google.com>
This is now implemented and documented. Closing. |
TL;DR
This is a restatement of proposal #52509 by @zephyrtronium. That issue has collected a lot of comments; starting anew will make it easier for readers to follow along again. The primary additional contributions in this issue are the formulation of the exact spec changes, a corresponding new API for go/types, and a discussion of these changes in the context of a future Go without the current implementation restrictions on interfaces. As a result, this issue is long, but the actually proposed change is small.
Background
The Go 1.18 release introduced the predeclared identifier
comparable
which denotes the set of all non-interface types that are comparable. More precisely, it is the set of (non-interface) types for which==
and!=
are defined and for which these operations are guaranteed to not panic.The types in the
comparable
type set do not correspond to the types of comparable operands: for instance,struct{ f any }
is comparable but applying==
on the fieldf
may panic.For clarity, in the following we use the term strictly comparable for the types in
comparable
, and spec-comparable for types of comparable operands. Strictly comparable types are spec-comparable, but not the other way around. Types that are not spec-comparable are simply not comparable.Because in Go 1.18 and Go 1.19 constraint satisfaction means interface implementation, not all spec-comparable types satisfy the
comparable
constraint. The generic map typecannot be instantiated with
struct{ f any }
forK
, but the built-inmap
type permits that. This is a major handicap for the design of generic data structures. See #51257 for another example.Over the course of this year, many ideas have been proposed to address this problem, while trying to preserve the rule that constraint satisfaction and interface implementation remain the same. Here's a (possibly incomplete) list of proposals for reference: #51338, #52474, #52531, #52614, #52624, #53734.
If we want
any
to satisfycomparable
, then constraint satisfaction can't be the same as interface implementation. A non-comparable typeT
(say[]int
) implementsany
, butT
does not implementcomparable
(T
is not incomparable
's type set). Thereforeany
cannot possibly implementcomparable
(the implements relation is transitive - we cannot change that). So if we wantany
to satisfy thecomparable
constraint, then constraint satisfaction can't be the same as interface implementation.A pre-Go1.18 implementation of the compiler did not treat interface implementation and constraint satisfaction the same, which led to initial confusion because of the resulting inconsistency (see #50646). Eventually the inconsistency was considered an implementation bug (in hindsight this was done without enough consideration of all the implications). With more experience it appears that this inconsistency made it easier to use generic Go with commonly used Go types.
@zephyrtronium's #52509 and this issue propose that we go back to this pre-Go1.18 implementation of constraint satisfaction.
Proposal
We change the spec to use a different rule for constraint satisfaction than for interface implementation: we want spec-comparable types to satisfy
comparable
; i.e., we want to be able to use any type for which==
is defined even if it may not be strictly comparable.Specifically, in the language spec section on Instantiations, we propose to change the 2nd rule from (old):
to (new):
We also add a new paragraph defining what "satisfy" means:
With this change, constraint satisfaction matches interface implementation but also contains an exception for spec-comparable types. This exception permits the use of interfaces as type arguments which require strict comparability.
This is the entire proposal.
Observations
The proposed change expands the set of types that satisfy a constraint, therefore no existing programs are invalidated and the proposal is fully backward-compatible. See the section on Implementation below for when the feature becomes available.
The spec has various implementation restrictions: interfaces that are not basic may only be used as type constraints, or as elements of other interfaces used as constraints. They cannot be the types of values or variables, or components of other, non-interface types. Because of these restrictions, if the type argument
T
is a (non-type parameter) interface, it must be a basic interface. Thus, we can ignore the case of type-constrained interfaces (such asinterface{ ~[]int }
) which can only hold non-comparable types. For the same reason, ifT
is a non-interface type with an interface field (such asstruct{ f F }
whereF
is an interface),F
must be a basic interface. In other words, we don't need to complicate the rules to explicitly disallow such types forT
: any interface that is or appears in a (non-type parameter) type argument must be a basic interface and therefore is spec-comparable.The spec has additional implementation restrictions in place:
comparable
and interfaces containing methods may not appear as terms in a union (with more than one term). Because of these restrictions, ifcomparable
appears in an interface (or an embedded element), it can be "factored out to the top"; the same is true for methods. More precisely, constraint interfaces can always be written in one of two forms:interface{ comparable; E }
orinterface{ U; E }
, whereE
stands for all the interface's methods (if any), andU
denotes a union of non-interface types (if any). (An interface of the forminterface{ comparable; U; E }
can always be reduced to the forminterface{ U'; E }
by eliminating all non-strictly-comparable terms fromU
).For
interface{ U; E }
constraints, constraint satisfaction is the same as interface implementation: nothing changes. For a typeT
to satisfy the constraint, it must be in the union and satisfy all the methods, if any.For
interface{ comparable; E }
constraints, constraint satisfaction is looser: we still expect a type argumentT
to implement all the methodsE
, but we allow anyT
which is spec-comparable, even if it is not strictly comparable. This is not statically type-safe and must be remedied with a run-time check for==
.Because the new rule invalidates static type safety, the definition of strictly comparable is not quite correct anymore:
==
on an operand of a strictly comparable type parameter type may in fact panic at run-time. To be able to separate the various kinds of comparability, strictly comparable now more precisely denotes the set of types for which==
and!=
are defined and for which these operations won't panic if type safety was never broken through the instantiation of a (strictly) comparable type parameter with a spec-comparable type argument.If a type parameter
P
is spec-comparable, it is always strictly comparable (but see the previous observation). Therefore, if a type parameter is used as a type argument for a constraint of the forminterface{ comparable; E }
, the rule thatP
must "only" be spec-comparable doesn't weaken any static type guarantees becauseP
will be strictly comparable.The pre-Go 1.18 implementation, before the inconsistency documented in spec: document/explain which interfaces implement
comparable
#50646 was reported, matched the proposed rules.Eventually we may want to remove the existing implementation restrictions. This will also require a generalization of the rule for constraint satisfaction. To ensure backward compatibility, the proposed rule here should match a future, more general rule adjusted for the current implementation restrictions. But at the very least, the proposed rule should not permit more programs now than might be permitted with a more general rule for constraint satisfaction. See the appendix for an attempt at such a generalization.
Examples
Currently,
any
does not implement thecomparable
constraint. With the proposed changeany
will be permitted as type argument forcomparable
:comparable
can be written asinterface{ comparable; E }
and thus the new rule applies, andany
is spec-comparable and implementsE
(whereE
is the empty interface in this case).Currently, the type parameter
P
in the type parameter listcannot be instantiated with the type
S
because
S
is not strictly comparable. With the proposed change,S
must only be spec-comparable (which it is) and implementfmt.Stringer
(which it does).More examples
Implementation
The proposal requires minor changes to the typechecker:
-altcomparable
enables the new behavior (disabled by default):If the proposal is accepted for Go version 1.nn, we will replace this flag with a version check that enables the new behavior starting with Go 1.nn.
types.Implements
function without breaking backward-compatibility. Instead, we propose to add the following exported function:types.Comparable
that implements the notion of strict comparability. We can wait with this until a clear need arises.All these changes are trivial (the internal implementations exist, they just need to be exported).
If this proposal is accepted in time, it could become effective with the Go 1.20 release.
Related spec issues
comparable
is incorrect in the spec. The definition is as follows:The first rule (
T
supports the operations==
and!=
) also includes types (such as structs) which may not be strictly comparable (see the structs used in the examples above). The rule needs to be amended to exclude types for which==
may panic.Both these spec corrections should be made irrespective of whether this proposal is accepted or not.
Appendix
In a hypothetical future version of Go ("FutureGo") where generalized interfaces may be used as ordinary types, the constraint satisfaction rules must be suitably extended and expressed in terms of type sets. @ianlancetaylor proposes the following FutureGo rules:
Given these special type sets
S1
andS2
, we can define general constraint satisfaction as follows:(The case for type parameters can be folded into the case of non-interface types, but for the following discussion it is clearer to handle it separately.)
If
T
is not an interface (or type parameter) type, it is not hard to see why the respective sub-rule makes sense: instead of only accepting strictly comparable types forcomparable
, the use of the type setS1
permits any spec-comparable type.If
T
is an interface type, we also want to accept it as a type argument forcomparable
: more precisely, any interface type, irrespective of its type set, supports==
and thus is spec-comparable (we could say that non-basic interfaces don't support==
by default, but that's an separate discussion). Effectively this means thatcomparable
is the same asany
in this case, hence the type setS2
. However, we (probably) want to exclude interface types with type sets for which we can prove that==
can't possibly succeed. For instance,interface{ ~[]int }
is spec-comparable (all interfaces support==
, but see the comment on that above), but its type set only contains non-comparable types: comparing operands of typeinterface{ ~[]int }
will always panic. The intersection of the type set of such interfaces with the type setS1
will be empty (if it were not empty, the interface would contain at least one spec-comparable type and so==
might always happen to succeed in a concrete application). This explains the second part of the rule for interfaces.Finally, if
T
is a type parameter, nothing changes and we can simply ignore this case below.To make sure we remain forward-compatible with FutureGo, i.e., that these generalized constraint satisfaction rules do not restrict our current, limited proposal, we need to show that our current proposal doesn't permit more types for a constraint than the FutureGo rules.
We can do this by applying the current implementation restrictions to the FutureGo constraint satisfaction rules. With those restrictions in place, from the Observations section we know that constraint interfaces can appear in only two (canonical) forms:
a)
interface{ comparable; E }
; orb)
interface{ U; E }
where
E
stands for all the methods in the constraint (if any), andU
denotes a union of non-interface types (if any). Therefore, the respective type setS1
is eitherS1a
which is the type set ofinterface{ comparable; E }
withcomparable
denoting all spec-comparable types; orS1b
which is the type set ofinterface{ U; E }
(unchanged).Similarly, the type set
S2
is eitherS2a
which is the type set ofinterface{ E }
; orS2b
which is the type set ofinterface{ U; E }
(unchanged).Now we can look at the type argument
T
and how the generalized rules apply:If
T
is not an interface type, per the generalized rules,T
must be an element ofS1
. Therefore,T
must either be an element ofS1a
, orS1b
. If the former is true,T
must be spec-comparable and implement the methodsE
- this corresponds to the our proposed exception for interface satisfaction. If the latter is true,T
is simply an element of the type set of the constraint, which meansT
implements the constraint. This corresponds to the non-exceptional part of the proposed rule for constraint satisfaction.If
T
is a (non-type parameter) interface type, with the implementation restrictions in place,T
must be a basic interface. Per the generalized rules, the type set ofT
must be a subset ofS2a
and the intersection ofT
's type set andS1a
must not be empty, or the type set ofT
must be a subset ofS2b
and the intersection ofT
's type set andS1b
must not be empty. If the former is true,T
implements the methodsE
, and becauseT
is a basic interface it is also spec-comparable. This corresponds to the proposed exception for constraint satisfaction. If the latter is true,T
simply implements the constraint, which again corresponds to the non-exceptional part of the proposed rule for constraint satisfaction.In summary, if we apply the current implementation restrictions to FutureGo, the FutureFo constraint satisfaction rules reduce to the rules proposed in this issue. This provides some insurance that the proposed rules won't cause problems in FutureGo.
The text was updated successfully, but these errors were encountered: