-
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: allow interface types to instantiate comparable
type parameters
#52509
Comments
I agree with this proposal as it is simpler than the others. For code that would like to have a call comparable that cannot panick, we can add a strict_comparable (or the same with a better name) predefined interface. |
Change https://go.dev/cl/401874 mentions this issue: |
I was prototyping a spec change, and it seems compatible with the goal of this proposal, although the implementation goes more into the detail of differentiating type sets and interfaces. To advocate stopping those incomprehension discussions, we should clarify what exactly is the problem, and I think the problem is we are limiting ourselves to a mindset that type set and interfaces are the same things, which it turns out: they are different. Again: Interface is just an approach to define a type set, and it can embed another type set, which may be an interface, comparable, or any other future possible predeclared type set. Therefore, in CL 401874, I attempt to clarify this and remove comparable from a predeclared type to a predeclared type set. As a side effect, func f[T any](x T) {} // any as type set
func g[T comparable](x T) {}
func h(x any){} // any as function parameter
var (
x func() = func() {}
_ = f(x) // OK, T is infered as func(), and f is instantiated as f(x func())
_ = g(x) // Invalid, type func() is not comparable.
_ = h(x) // OK, h accepts anything
) Also, this behavior remains the same as expected: var (
anyType = types.Universe.Lookup("any").Type()
comparableType = types.Universe.Lookup("comparable").Type()
)
fmt.Println(types.AssignableTo(comparableType, anyType)) // true
fmt.Println(types.AssignableTo(anyType, comparableType)) // false |
@changkun FWIW I disagree that the core issue is one of how to word things. I believe the core issue is what behavior we want, i.e. a) do we want |
@Merovius I think the wording address what we might want explicitly:
|
@changkun What you are not doing is talking about what kind of code we want to be able to write and what that code should or should not do. #51338 and this proposal both try to address a specific problem: There is no way to instantiate e.g. None of these technical questions changes based on whether we call IMHO one reason these discussions have become so long and convoluted is because people try to interpret what "comparable" means and/or trying to come up with new definitions of that term, instead of talking about the concrete technical questions which are on the table. Namely, what code do we want to be able to write and what should that code do. |
So would interface types be included in What would it entail? |
Yes.
I don't understand the question. |
So now how would you define interface implementation in terms of typeset? |
@atdiar I don't think we should worry about how to word the change, before we have decided what the change is we want to make. It doesn't seem particularly hard to word this, to me. |
It's not just about wording. It affects how we understand, compute and use typesets. Currently the spec defines an interface T implementing another interface I as:
How would you change it? |
If you have questions about how a specific piece of code would behave under this proposal, or how a specific piece of code could be written, I feel more confident that I could answer. Personally, as I said, I think it detracts from the discussion to talk about wording (but, if someone else thinks differently, they might well try to take a stab at it, of course). |
I am not because people are mixing the objectives and concepts here. Of course I understand there is no way to instantiate a vague conceptual "comparable" type set The language had a definition of "comparable" types, and
Is it a problem with "what we want to write", or "what people would actually do"?
and people erase the type information using any:
Is the panic a language issue or a usage issue?
No. I think all of these questions arise because people start to talk about a solution without defining the scope or understanding what it exactly means. Again, the problem would be framed completely differently if the concepts are clarified. The problem would be:
Exactly! But how do all these questions is strong associating with the identifier |
"We want to be able to instantiate That particular problem is addressed by #51338 (after embedding But the problem statement is very clear: We want to write specific generic functions, which are currently impossible to write. |
Still, I have the feeling that you maintained in the mindset that type set and interfaces are the same. As the CL attempt to clarify: interface can be used to define a type set. But there are type set that cannot be written or implemented using interface, such as If we want to solve a particular problem to instantiate proposal: spec: add a predeclared type set so that we can
Of course there are so many generic function we want to write and currently impossible to write. But why mix up with the comparable? |
I'm still asking because without proper definitions, we don't know what we are talking about. So under the proposed, updated, type M struct{
Name string
Map map[string] interface{}
}
type Set[T comparable]... How is it decided that Do we still use typeset inclusion? |
This is a joke: maybe we should revisit the previous abandoned contract design. Now we know that there is a clear difference between what is a contract and what is an interface. Because interface can implement a contract, but a contract is a contract. |
That might be a solution to the problem. Feel free to file such a proposal. I disagree that it is better to say that is the problem. The problem is "we can't instantiate |
The spec defines which types are comparable. Map types are categorically not comparible, so neither are struct types with map fields. This proposal only suggests changing whether or not interface types do or do not implement |
Maybe. But that's not a strong enough argument to complicate the definition "we can't instantiate not "we can't instantiate |
Just follow me for now. By the spec definition, M also implements Should we use typeset inclusion to constrain type parameters or is there now something else we use? |
@atdiar FWIW the proposal is to strike the "non-interface" and "is not an interface type" from the section about But again, that doesn't matter right now. We need to decide if we want to do this. We can always figure out how to word it, after we decided that. We can always fix ambiguities and conflicts, once we actually know how we want the language to behave. We can find words to describe whatever semantics we want. We are doing that all the time. It's just not a problem. |
@changkun I think the spec changes you propose contain some unneeded changes as well. We came from contracts to interfaces to interfaces as type sets, I don't think we can go back now easily. The only change we need is this one: The predeclared identifier comparable is a type set that denotes the set of all types that are comparable (this "comparable" refers to the Go 1's definition of types that are comparable). |
@Merovius , @atdiar what are you going on about? You can agree with this proposal or oppose it but it seems besides the point. This proposal is to make the generic comparable and the pre generic comparable identical, for simplicity. It will have the benefit of making generics much easier to reason about. |
I disagree. We can't sweep everything under a rug. There are moving pieces and changing one of them may impact everything else. You asserted that this change would also mean that interface types are part of The spec clearly says that:
With the proposed change, M would therefore be a member of I am merely asking, how do you propose to resolve this incongruency? I have an idea that I have already mentioned in other issues. But that seems to have flown over people's head too. So we either keep @beoran there is an issue with this proposal and I am trying to explain why I think so. The end goal may still be legible. |
@beoran I understand that. FWIW it was my first suggestion when the problem came up as well. |
I think the rule is " |
It means this compiles and works (which is good, as that's the reason for this proposal): type Set[K comparable] map[K]struct{}
func main() {
s := make(Set[any]) // instantiate `Set` using `any`. any ∈ typeSet(comparable), so this is allowed by rule v.
} |
Change https://go.dev/cl/444635 mentions this issue: |
Change https://go.dev/cl/444636 mentions this issue: |
The 2 CLs above essentially get us back to before #50646 was addressed (by strictly implementing the typeset rules), and re-introduce the "inconsistency" that existed at that time. Specifically, an ordinary interface satisfies the comparable constraint. I believe that's the essence of this proposal. The CLs allow us to experiment with this approach. |
This is an experiment to see the impact of a potential spec change: As an exception to the rule that constraint satisfaction is the same as interface implementation, if the flag Config.AltComparableSemantics is set, an ordinary (non-type parameter) interface satisfies the comparable constraint. (In go/types, the flag is not exported to avoid changing the API.) Disabled by default. Test files can set the flag by adding // -altComparableSemantics as the first line in the file. For #52509. Change-Id: Ib491b086feb5563920eaddefcebdacb2c5b72d61 Reviewed-on: https://go-review.googlesource.com/c/go/+/444635 Reviewed-by: Robert Findley <rfindley@google.com> Run-TryBot: Robert Griesemer <gri@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: David Chase <drchase@google.com>
Add the experimental compiler flag -altcomparable. If set, the compiler uses alternative comparable semantics: any ordinary (non-type parameter) interface implements the comparable constraint. This permits experimenting with this alternative semantics akin to what is proposed in #52509. For #52509. Change-Id: I64192eee6f2a550eeb50de011079f2f0b994cf94 Reviewed-on: https://go-review.googlesource.com/c/go/+/444636 Run-TryBot: Robert Griesemer <gri@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@google.com> Auto-Submit: Robert Griesemer <gri@google.com> Reviewed-by: Robert Griesemer <gri@google.com> Reviewed-by: Robert Findley <rfindley@google.com>
I just posted #56548 which in essence is this proposal, but also includes the precise spec change and a discussion. @zephyrtronium, please comment as you see fit (and/or perhaps close this proposal, if you're so inclined). Thanks. |
I'll close this in favor of #56548. If that proposal doesn't work out, this one can be reopened. |
This is an experiment to see the impact of a potential spec change: As an exception to the rule that constraint satisfaction is the same as interface implementation, if the flag Config.AltComparableSemantics is set, an ordinary (non-type parameter) interface satisfies the comparable constraint. (In go/types, the flag is not exported to avoid changing the API.) Disabled by default. Test files can set the flag by adding // -altComparableSemantics as the first line in the file. For golang#52509. Change-Id: Ib491b086feb5563920eaddefcebdacb2c5b72d61 Reviewed-on: https://go-review.googlesource.com/c/go/+/444635 Reviewed-by: Robert Findley <rfindley@google.com> Run-TryBot: Robert Griesemer <gri@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: David Chase <drchase@google.com>
Add the experimental compiler flag -altcomparable. If set, the compiler uses alternative comparable semantics: any ordinary (non-type parameter) interface implements the comparable constraint. This permits experimenting with this alternative semantics akin to what is proposed in golang#52509. For golang#52509. Change-Id: I64192eee6f2a550eeb50de011079f2f0b994cf94 Reviewed-on: https://go-review.googlesource.com/c/go/+/444636 Run-TryBot: Robert Griesemer <gri@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@google.com> Auto-Submit: Robert Griesemer <gri@google.com> Reviewed-by: Robert Griesemer <gri@google.com> Reviewed-by: Robert Findley <rfindley@google.com>
A note for future reference. I started sketching a proof that #52509 (comment) and the current language of #56548 allow for the same assignments and instantiations, and I found a surprise. After some simplification, the rule for assignability of values x of concrete type T to variables of parameter type P reads as "for all types U in the type set of P, x is assignable to U." Say P is constrained by the interface I. By @Merovius's rules, the type set of P contains not only I but every interface with at least the methods of I, which means in particular that the type set of P contains mutually exclusive interfaces. For any given interface type U in P, T must be a member of the type set of U for x to be assignable to U. Since the type set of P contains mutually exclusive interfaces, there can be no T assignable to all types in the type set of P. So, it seems we cannot assign any value of any concrete type to any variable of parameter type. It seems like what #52509 (comment) wants for type parameters is something like the assignable closure. A concrete type T needs to be a member of cl(P), an interface type I needs to be a subset of cl(P) (which is a contradiction, since I contains interface types! But that is what we want anyway, otherwise we could write Similar to one of the notes in #52509 (comment), I do believe that defining the assignable closure for concrete types means that assignability is always exactly the subset relation between closures. That might make it easier to formalize the rules for unnamed types, directional channels, and so on. |
I don't think I wrote down a rule for that. It also seems a rule of dubious utility. I think that's what you essentially showed. We might be able to write a rule allowing this, but it would require There is a rule for a concrete type instantiating a type parameter, but that rule uses ∈ and again doesn't seem to fit your argument. What am I missing? What specific rule are you referring to (they are numbered)? |
That's true. I was so focused on resolving the contradiction that I didn't think about whether it's correct to do so, and indeed, we would be able to write |
Change https://go.dev/cl/453978 mentions this issue: |
Change https://go.dev/cl/453979 mentions this issue: |
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>
Background
Out of caution over backward compatibility of features introduced with generics, with little discussion with the community when changing a major proposal that had already been accepted, Go 1.18 defined the predeclared constraint
comparable
to be "the set of all non-interface types that are comparable." The majority of the conversation motivating the "non-interface" portion of that definition occurred in #50646 and #49587.That interface types are comparable but not
comparable
is a significant pain point for a number of otherwise fine uses of generics: e.g., one cannot use x/exp/maps algorithms on map types which havereflect.Type
as keys. #52474 (now retracted) was proposed to alleviate this problem, noting that the precise definition ofcomparable
should include types like[1]reflect.Type
, which is an array type supporting==
rather than an interface type. A significant portion of the comments on that proposal noted that the entire motivation for the proposal is the inconsistency between "comparable" andcomparable
.Proposal
The proposal here is to allow interface types to instantiate type parameters constrained by
comparable
. In essence, I propose to remove the term "non-interface" from the definition ofcomparable
, so that "comparable" andcomparable
mean the same thing in every context in Go.@atdiar points out that other language in the spec would produce contradictions following that simple change. This proposal would additionally require a change to the definition of type sets or the definition of implementing interfaces, likely splitting either or both into two senses for type parameters and otherwise.
A consequence of this proposal is that it becomes possible to write generic code using
comparable
which panics on comparison with non-comparable concrete types. That is an aspect of the type system which has existed since long before Go 1.0; in particular, see @nield's demonstration that the results are very similar to the situation we've always had. What we gain is the ability to write code generic over all comparable types, rather than most of them with no solution for the remainder.For the concrete change to the Go specification that I propose to implement this, see #52509 (comment). It is not the only possible approach. @jimmyfrasche proposes #52509 (comment), and @Merovius proposes #52509 (comment) with an enumeration of examples at #52509 (comment).
Related Proposals
The text was updated successfully, but these errors were encountered: