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. |
Note that testing the dynamic type of a value is in general not sufficient to determine whether a comparison would panic (https://go.dev/play/p/TYXz4jEaND4). Any such check would have to depend on the concrete value itself, not just its dynamic type. |
I've filed #52624 as a counterproposal that does provide a type-assertion sufficient to prevent run-time panics. |
Here is a specification change I believe would implement this proposal. We introduce a new relation 'satisfies' between types. The section Implementing an interface becomes (italic indicates changes):
Then, the section Instantiations changes:
In writing this, I tried to achieve three goals:
In general, I believe any implementation of this proposal which avoids contradictions will require either a new relation between types and interfaces or a new thing which is a type set but not an interface. The latter is related to #52531, and @atdiar has suggested it more directly in comments elsewhere. I prefer the former, because I think it leaves more room for a consistent realization of #51338 and any other future proposal to allow values of interface types which currently cannot have values. If we do not add the 'satisfies' relation but add its extra rule to 'implements', then I believe we get something quite like #52614. |
I would define implements in term of satisfies: A type A type
I would keep your s/implements/satisfies/ edit to instantiations. I would and define (somewhere) that the type set of comparable is the subset of types that match the definition of comparable in https://go.dev/ref/spec#Comparison_operators Interfaces are still in the type set of comparable but they're also in the type set of any. That's okay because the division between This would mean that It may be possible to define a stricter type set that's the "safe" subset of comparable but, if so, I think that should be given a separate name as it is a new concept that's not already in wide use. |
@griesemer from #52614:
Would the change I sketched in the post above provide a sufficient mechanism? |
I don't know (I'd have to read the entire proposal again and apply all comments to deduce the latest rules.) But from a 10,000 ft perspective it seems to me that using two different relations ("satisfies" vs "implements") depending on context is going to lead to trouble. We have concrete types (T), interfaces (I) and type parameters (P) and we have to give rules for all combinations: T rel I, T rel P, I rel I, I rel P, P rel I, P rel P, where rel is one of "implements" or "satisfies" . It would be helpful if somebody who believes they understand this proposal wrote not only explanations for what "implements" and "satisfies" are supposed to mean but also how they apply for all these combinations (and ideally show that they cannot lead to inconsistencies). For comparison, in the Go spec right now we have only one case: "implements" and "satisfies" mean exactly the same, and the meaning is always the same no matter if we have concrete types, interface types, or type parameters: x implements y if the type set of x is a subset of y. (The type set of a concrete type is just that type.) |
I think the way I would implement this is
(note for clarity: A "type parameter" is, to me, what is in the type parameter list in the function declaration. A "type argument" is what was passed to the instantiation of the function. I think that terminology is established, but just to be sure) This doesn't use "implements" and "satisfies", but the meaning is kind of the same. It would require writing down the cases in the appropriate sections (i.e. in the assignability section), instead of making up a term for one of these relationships and then using that term to say when an assignment is allowed. It might be possible to still do that. Perhaps we can use "implements" or "satisfies" for It would be possible to add Above is, I think, the behavior I would expect from this proposal. But I'm open to be convinced that it's still somewhat handwavy in parts, or that it does not match my expectations when taken at face value. It feels reasonably simple and straightforward to me, though. One crucial deficiency of this is that it requires that |
Haven't had a chance to scribble anything down yet but that looks basically like what I planned to do. Did you mean ⊆instead of ⊂though? I believe (but have not thought about enough to strongly believe) that it should be possible to check if X⊆Y easily as it naturally breaks down into cases that are easy to check. |
Sure, why not :) Conventions are not entirely consistent, some mathematicians use
FWIW, there are potential pitfalls because of implicit negative constraints. For example, having a method |
@Merovius In the rules you wrote down, is an interface To put it another way, is this valid? type I interface { ~int | ~string }
func F1[T I](v T) {}
func F2[T I](v T) { F1(v) } |
@ianlancetaylor the call to |
@ianlancetaylor What @jimmyfrasche said. I don't tend to think of interfaces with type elements as "types", which is why I didn't think of spelling that out. But yes, "basic interfaces" would always be in their own type set, whereas interfaces with type elements generally would not. I guess we'd have to argue about whether |
And FWIW, the fact that basic interfaces are always in their own typeset follows from the same rules that imply today, that an interface type always implement itself. A basic interface is defined by its method set and a basic interface's method set contains all method it requires. |
Oh and, as another note: Interfaces with type elements only ever appear in the rules in the form of a |
Ignoring unions containing interfaces for the moment,
If, aside from the restriction on assignable types, they are otherwise ordinary interface values, then it seems like they're covered by rules i and ii. |
If Observe that
The rules would then imply that
I think these are exactly the intuitive results we would want in that situation. |
Typo, wrong way around, I assume ( |
Can you provide a code sample to describe this? I'm not sure I understand it correctly. |
@Merovius Right now we say that an operation (say Do I understand that correctly? |
Doesn't that fall out of |
Here's an attempt at a summary, slightly more compact, written down for my own understanding:
The differences between this and what we have now are:
Do I have this correctly? |
I suppose so, but I didn't see this written down. Is it written down somewhere (besides in the summary I just wrote)? |
Yes, I think so.
As part of the discussion, e.g. this comment. I wasn't explicit about it, but it's a consequence of the |
I think Otherwise the instantiation rule doesn't work out. |
Good point. This is BTW an issue with the spec right now:
This would include interfaces in type sets, which we tried to leave out by construction. So, currently there should be a "non-interface" added here. i.e. making that work seems to be a pretty natural consequence of defining type sets based on their elements - it has to be actively excluded, not to be the case. |
This was fixed several weeks ago. You're not looking at the spec at tip:
|
I've been thinking about the rules in #52509 (comment) through the lens of #52624. If we treat For this proposal, I believe the rule is (roughly):
That is, in the program: type SemiComparable struct { N int, I interface{} }
…
s := SemiComparable{ N: 42, I: func() {} }
var a any = s
c, ok := a.(comparable) // c = s; ok = true
_ = c == s the program would compile successfully, the type-assertion would succeed, and the comparison would fail at run-time. |
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.
} |
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: