Skip to content
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

Open
zephyrtronium opened this issue Apr 23, 2022 · 99 comments
Labels
generics Proposal
Projects
Milestone

Comments

@zephyrtronium
Copy link
Contributor

@zephyrtronium zephyrtronium commented Apr 23, 2022

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 have reflect.Type as keys. #52474 (now retracted) was proposed to alleviate this problem, noting that the precise definition of comparable 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" and comparable.

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 of comparable, so that "comparable" and comparable 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

@beoran
Copy link

@beoran beoran commented Apr 23, 2022

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.

@gopherbot
Copy link

@gopherbot gopherbot commented Apr 23, 2022

Change https://go.dev/cl/401874 mentions this issue: spec: clarify type set and interface are different

@changkun
Copy link
Member

@changkun changkun commented Apr 23, 2022

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, comparable is now aligned with the original type parameter proposal, that: 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). For the differences between any and comparable, simply just: depending on how they are used:

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 changkun added the generics label Apr 23, 2022
@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

@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 comparable to be usable as a type (#51338), b) do we want interface types to implement the comparable constraint (this proposal) and c) do we maybe want to do other changes to solve other problems associated with either (e.g. #52474, which tries to address a problem with #51338). Once we decided on the behavior we want, finding the right words to put into the spec should be easy. But we should really focus on the actual technical language changes we want, first.

@changkun
Copy link
Member

@changkun changkun commented Apr 23, 2022

@Merovius I think the wording address what we might want explicitly:

  • a) we disallow comparable to be used as a type (as they are not today)
  • b) interfaces are comparable and satisfy comparable type set (as they are in Go 1)
  • c) the problems are not valid if type set is just a type set. (as the generics design doesn't permit using interface to be Turing complete for defining all kinds of type sets)

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

@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. type Set[K comparable] … using interface-types. They do that using concrete technical changes, which have their own, technical drawbacks, namely "comparable becomes viral" in the case of #51338 and "comparisons in generic functions might panic" in the case of this proposal. If we want to enable people to use code like Set[reflect.Type], we have to address and/or weigh these drawbacks.

None of these technical questions changes based on whether we call comparable a "constraint", a "type set" or a "type" (though TBF, we use "comparable is a type" as a short-cut for #51338). It doesn't help to demonstrate that a particular answer can be worded in the spec. We need arguments as to why a particular answer is better than the others.

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.

@atdiar
Copy link

@atdiar atdiar commented Apr 23, 2022

So would interface types be included in comparable's typeset?

What would it entail?

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

@atdiar

So would interface types be included in comparable's typeset?

Yes.

What would it entail?

I don't understand the question.

@atdiar
Copy link

@atdiar atdiar commented Apr 23, 2022

So now how would you define interface implementation in terms of typeset?

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

@atdiar
Copy link

@atdiar atdiar commented Apr 23, 2022

It's not just about wording. It affects how we understand, compute and use typesets.
So I will rephrase because it is important.

Currently the spec defines an interface T implementing another interface I as:

T is an interface and the type set of T is a subset of the type set of I.

How would you change it?

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

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).

@changkun
Copy link
Member

@changkun changkun commented Apr 23, 2022

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. type Set[K comparable] … using interface-types.

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 type Set[K comparable] …. But it has no association with the existing comparable identifier, the issue is: how such a type set looks like, and what should be the name of such a type set.

The language had a definition of "comparable" types, and comparable is the set of types that are comparable.

They do that using concrete technical changes, which have their own, technical drawbacks, namely "comparable becomes viral" in the case of #51338 and "comparisons in generic functions might panic" in the case of this proposal. If we want to enable people to use code like Set[reflect.Type], we have to address and/or weigh these drawbacks.

Is it a problem with "what we want to write", or "what people would actually do"?
If we use type set comparable:

func eq[T comparable](x, y T) bool { return x == y }

and people erase the type information using any:

var _ = eq(any(func(){}), any(func(){})) // eq(any, any)

Is the panic a language issue or a usage issue?

None of these technical questions changes based on whether we call comparable a "constraint", a "type set" or a "type" (though TBF, we use "comparable is a type" as a short-cut for #51338). It doesn't help to demonstrate that a particular answer can be worded in the spec. We need arguments as to why a particular answer is better than the others.

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: proposal: spec: add new predeclared type set X, Y, Z

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.

Exactly! But how do all these questions is strong associating with the identifier comparable then?

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

@changkun

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.

"We want to be able to instantiate type Set[K comparable] … using reflect.Type" is a precise and unambiguous problem-statement. It stands in for a wider range of problems, e.g. "we want to be able to write a maps.Keys function usable with a map[reflect.Type]T". It is not a solution, it is a problem. Specifically, a problem about particular pieces of code, which are currently impossible to write.

That particular problem is addressed by #51338 (after embedding comparable in reflect.Type) and it's addressed by this proposal, by allowing type parameters interface types to implement comparable. There might be other solutions as well, feel free to bring one up.

But the problem statement is very clear: We want to write specific generic functions, which are currently impossible to write.

@changkun
Copy link
Member

@changkun changkun commented Apr 23, 2022

"We want to be able to instantiate type Set[K comparable] … using reflect.Type" is a precise and unambiguous problem-statement. It stands in for a wider range of problems, e.g. "we want to be able to write a maps.Keys function usable with a map[reflect.Type]T". It is not a solution, it is a problem. Specifically, a problem about particular pieces of code, which are currently impossible to write.

That particular problem is addressed by #51338 (after embedding comparable in reflect.Type) and it's addressed by this proposal, by allowing type parameters interface types to implement comparable. There might be other solutions as well, feel free to bring one up.

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 comparable.

If we want to solve a particular problem to instantiate type Set[K comparable] … using reflect.Type, it is better to clarify that the problem is:

proposal: spec: add a predeclared type set magicset

so that we can type Set[K magicset] ... and reflect.Type is an element of the type set magicset.

But the problem statement is very clear: We want to write specific generic functions, which are currently impossible to write.

Of course there are so many generic function we want to write and currently impossible to write. But why mix up with the comparable?

@atdiar
Copy link

@atdiar atdiar commented Apr 23, 2022

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'm still asking because without proper definitions, we don't know what we are talking about.

So under the proposed, updated, comparable,

type M struct{
   Name string
   Map map[string] interface{}
} 
type Set[T comparable]... 

How is it decided that M cannot be a value for the type argument?

Do we still use typeset inclusion?

@changkun
Copy link
Member

@changkun changkun commented Apr 23, 2022

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.

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

If we want to solve a particular problem to instantiate type Set[K comparable] … using reflect.Type, it is better to clarify that the problem is:

proposal: spec: add a predeclared type set magicset

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 type Set[K comparable] … using reflect.Type" as an example of a broader problem of using interface types in generic code which needs to do comparisons.

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

@atdiar

How is it decided that M cannot be a value for the type argument?

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 comparable, it does not talk about map types. So your example seems irrelevant to this discussion.

@changkun
Copy link
Member

@changkun changkun commented Apr 23, 2022

I disagree that it is better to say that is the problem. The problem is "we can't instantiate type Set[K comparable] … using reflect.Type" as an example of a broader problem of using interface types in generic code which needs to do comparisons.

Maybe. But that's not a strong enough argument to complicate the definition comparable type set. Again, the problem is:

"we can't instantiate type Set[K someconstraint] … using reflect.Type, someconstraint maybe named as comparable or different. If named as comparable, then we have to change comparable"

not

"we can't instantiate type Set[K comparable] … using reflect.Type"

@atdiar
Copy link

@atdiar atdiar commented Apr 23, 2022

@atdiar

How is it decided that M cannot be a value for the type argument?

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 comparable, it does not talk about map types. So your example seems irrelevant to this discussion.

Just follow me for now. By the spec definition, M also implements comparable since it implements any and any is implementing comparableso what gives?

Should we use typeset inclusion to constrain type parameters or is there now something else we use?

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

@atdiar FWIW the proposal is to strike the "non-interface" and "is not an interface type" from the section about comparable. I disagree that with that change, M would implement comparable, as it is not an interface type and does not support == and !=.

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.

@beoran
Copy link

@beoran beoran commented Apr 23, 2022

@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).

@beoran
Copy link

@beoran beoran commented Apr 23, 2022

@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.

@atdiar
Copy link

@atdiar atdiar commented Apr 23, 2022

@atdiar FWIW the proposal is to strike the "non-interface" and "is not an interface type" from the section about comparable. I disagree that with that change, M would implement comparable, as it is not an interface type and does not support == and !=.

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.

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 comparable typeset.
That's problematic.

The spec clearly says that:

Implementing an interface
A type T implements an interface I if

T is not an interface and is an element of the type set of I; or
T is an interface and the type set of T is a subset of the type set of I.
A value of type T implements an interface if T implements the interface.

With the proposed change, M would therefore be a member of comparable typeset. (while also not being comparable by the spec definition)
So typesets would not be constraining anymore. That would be a failure of typeset as way toward bounded quantification.

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.
In short, the proposed change can't work if comparable is an interface. We would need to extend the concept of constraint to be a superset of constraint interfaces.

So we either keep comparable as it is and we add something else, or we change comparable but it requires more than just what is in this proposal.

@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.

@Merovius
Copy link

@Merovius Merovius commented Apr 23, 2022

@bcmills
Copy link
Member

@bcmills bcmills commented Apr 29, 2022

@atdiar

So there would be no way to check whether a comparison will panic by testing the dynamic type?

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.

@bcmills
Copy link
Member

@bcmills bcmills commented Apr 29, 2022

Note that testing the dynamic type of a value is in general not sufficient to determine whether a comparison would panic.

I've filed #52624 as a counterproposal that does provide a type-assertion sufficient to prevent run-time panics.

@zephyrtronium
Copy link
Contributor Author

@zephyrtronium zephyrtronium commented Apr 30, 2022

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):

Implementing and satisfying an interface

A type T implements an interface I if:

  • T is not an interface and is an element of the type set of I; or
  • T is an interface and the type set of T is a subset of the type set of I.

Additionally, T satisfies I if:

  • T implements I; or
  • T is an interface with at least one comparable type in its type set, every element of the type set of I is comparable, and the comparable subset of the type set of T is a subset of the type set of I.

Then, the section Instantiations changes:

  1. After substitution, each type argument must satisfy the constraint (instantiated, if necessary) of the corresponding type parameter. Otherwise instantiation fails.

In writing this, I tried to achieve three goals:

  • Interfaces may instantiate comparable type parameters.
  • Interfaces are not in the type set of comparable, to avoid the contradictions already known.
  • comparable remains an interface.

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.

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Apr 30, 2022

I would define implements in term of satisfies:

A type T satisfies an interface I if T is a member of the type set of I.

A type T implements an interface I if:

  • T is not an interface and T satisfies I; or
  • T is an interface and the type set of T is a subset of the type set of I

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 satisfies and implements makes it clear that interface types impose additional restrictions on the type set.

This would mean that comparable can result in runtime panics but since comparable is already defined in the spec and stdlib (reflect, go/types) this is nothing more or less than what we already deal with now with non-generic code.

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.

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented May 5, 2022

@griesemer from #52614:

We currently don't have a mechanism to describe type sets that include interfaces.

Would the change I sketched in the post above provide a sufficient mechanism?

@griesemer
Copy link
Contributor

@griesemer griesemer commented May 5, 2022

@jimmyfrasche

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.)

@Merovius
Copy link

@Merovius Merovius commented May 5, 2022

I think the way I would implement this is

  1. Allow type sets to include interface types. Define typeSet(I) for an interface type I based on the definition ("all types with methods X,Y,Z, having types one of L,~M,~N…"). typeSet(comparable) is the set of all comparable types.
  2. Say (using the naming conventions @griesemer sets out)
    1. var A T is assignable to I, if T∈typeSet(I)
    2. var A I1 is assignable I2, if typeSet(I1)⊂typeSet(I2). The assignment assigns the dynamic value of I1 (or nil) to the I2 (this guarantees that the dynamic type of an interface is never an interface type).
    3. var A P (with [P C]) is assignable to I, if typeSet(C)⊂typeSet(I). The assignment behaves as if the type argument was substituted (i.e. it assigns the dynamic value, if the type argument is an interface)
    4. var A T can instantiate F[B C], if A∈typeSet(C)
    5. var A I can instantiate F[B C], if I∈typeSet(C)
    6. P (with [P C1]) can instantiate F[_ C2] if typeSet(C1)⊂typeSet(C2). The instantiation behaves as if the type argument (for P) was substituted

(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 ∈typeSet(A) or ⊂typeSet(A) and then use that. If that can be done consistently. But I don't think it's vital to do that, a lot of the spec already does this kind of detailed listing.

It would be possible to add typeSet(T) = {T} for non-interface types. This would make T∈typeSet(I) equivalent to typeSet(T)⊂typeSet(I) and allow us to unify the assignability-rules into one (though it would be a bit awkward to talk about the assignment of dynamic values). That gets traded off with the instatiation rules though, where we specifically don't want to use for the interface case. The way it is written right now is more verbose, but IMO simpler.

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 typeSet(A) is efficiently computable (or at least that is efficiently decidable). I think the spec already makes that assumption, somewhat to my chagrin. I also think the assumption is probably correct, with the interfaces we can express today (but it might prevent some interfaces we want to express in the future, like type Stringish interface { fmt.Stringer | ~string }. But until we purge the notion of "type sets" from the spec, I'm not sure this is a problem specific to this proposal.

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented May 5, 2022

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.

@Merovius
Copy link

@Merovius Merovius commented May 5, 2022

@jimmyfrasche

Did you mean ⊆instead of ⊂though?

Sure, why not :) Conventions are not entirely consistent, some mathematicians use /, some use /, some use different symbols entirely. is mapped on my keyboard and convenient to type, and I've kind of gotten used to / anyways.

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.

FWIW, there are potential pitfalls because of implicit negative constraints. For example, having a method M() means you don't have a method M() int, or having a field M means you can't have a method M() and so on. I think we managed to get rid of them with the restrictions we put into place, but it's surprisingly subtle.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented May 5, 2022

@Merovius In the rules you wrote down, is an interface I always a element of typeset(I)? That's not obvious from your definition.

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) }

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented May 5, 2022

@ianlancetaylor the call to F1 succeeds by rule vi regardless. I don't think I should be in typeset(I) as it is defined as {T | underlying(T) = int} ⋃ {T | underlying(T) = string}

@Merovius
Copy link

@Merovius Merovius commented May 5, 2022

@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 type X interface { ~X } or type X interface { X } or type X interface { X | int } should be allowed, to make it possible for such an interface's typeset to contain itself. Probably not, though.

@Merovius
Copy link

@Merovius Merovius commented May 5, 2022

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.

@Merovius
Copy link

@Merovius Merovius commented May 5, 2022

Oh and, as another note: Interfaces with type elements only ever appear in the rules in the form of a typeSet(C)⊂typeSet(I) check, never in the form of a C∈typeSet(I) check. Which sort of reflects my bias of not thinking of them as "types" and implying that it does not matter whether or not they are in their own (or any other) typeset.

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented May 5, 2022

Ignoring unions containing interfaces for the moment, comparable and A | ~B make sense as types to me under rules like these.

comparable is any with the restriction that its dynamic type is comparable

A | ~B is an interface whose dynamic type is restricted to A or a type in T where T = {X | underlying(X) = B}.

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.

@Merovius
Copy link

@Merovius Merovius commented May 5, 2022

If comparable ever becomes a full type (i.e. we adopt #51338):

Observe that

  1. any ∈ typeSet(comparable). Interface types are comparable.
  2. typeSet(comparable) ⊂ typeSet(any) (obviously)
  3. typeSet(any) ⊄ typeSet(comparable). e.g. []byte is in typeSet(any), but not typeSet(comparable).
  4. comparable ∈ typeSet(comparable). comparable itself is an interface type, so it's comparable.
  5. And comparable ∈ typeSet(any) (obviously).

The rules would then imply that

  1. comparable is assignable to any (rule ii, observation 2)
  2. any is not assignable to comparable (rule ii, observation 3)
  3. any (as a type argument) can instantiate comparable (rule v, observation 1)
  4. comparable (as a type argument) can instantiate any (rule v, observation 5)
  5. comparable (as a type argument) can instantiate comparable (rule v, observation 4)
  6. A type parameter constrained on comparable can instantiate a generic function constrained on any (rule vi, observation 2)
  7. A type parameter constrained on any can not instantiate a generic function constrained on comparable (rule vi, observation 3)

I think these are exactly the intuitive results we would want in that situation.

@Merovius
Copy link

@Merovius Merovius commented May 5, 2022

@jimmyfrasche

or a type T where T = underlying(B).

Typo, wrong way around, I assume (underlying(T) = B)?

@DmitriyMV
Copy link

@DmitriyMV DmitriyMV commented May 5, 2022

@Merovius

any (as a type argument) can instantiate comparable (rule v, observation 1)

Can you provide a code sample to describe this? I'm not sure I understand it correctly.

@griesemer
Copy link
Contributor

@griesemer griesemer commented May 5, 2022

@Merovius Right now we say that an operation (say +) on a type parameter is permitted if all types in the type parameter's type set support the operation. But if a type set of a type parameter contains an interface, the type parameter cannot have any operations because ordinary interfaces don't have operations (ignoring ==). I assume this is why you say "basic interfaces would always be in their own type set, whereas interfaces with type elements generally would not"?

Do I understand that correctly?

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented May 6, 2022

Doesn't that fall out of X not being in typeset(X) given something like type X interface { A | B }?

@griesemer
Copy link
Contributor

@griesemer griesemer commented May 6, 2022

Here's an attempt at a summary, slightly more compact, written down for my own understanding:

  1. Terminology
    a) uppercase T, I, P stand for a concrete, interface, or type parameter type respectively
    b) lowercase t, i, p stand for variables of type T, I, P respectively

  2. Typesets
    a) typeset(T) = {T}
    b) typeset(I): defined like in the Go spec but if I is a basic interface (no type elements), then typeset(I) also contains all interface types that implement I (i.e., have at least the same methods as I) [corrected].
    c) typeset(P) = typeset(I) with P declared as [P I]
    d) typeset(x) = typeset(type(x)) with x one of t, i, p

  3. Assignment
    a) x is assignable to i if typeset(x) ⊆ typeset(i)

  4. Instantiation
    a) T instantiates P if T ∈ typeset(P)
    b) I instantiates P if I ∈ typeset(P)
    c) P1 instantiates P2 if typeset(P1) ⊆ typeset(P2)

The differences between this and what we have now are:

  • the typeset of basic interfaces also contain all interfaces implementing those basic interfaces
  • instantiation with a non-type parameter uses type inclusion, not typeset inclusion

Do I have this correctly?

@griesemer
Copy link
Contributor

@griesemer griesemer commented May 6, 2022

@jimmyfrasche

Doesn't that fall out of X not being in typeset(X) given something like type X interface { A | B }?

I suppose so, but I didn't see this written down. Is it written down somewhere (besides in the summary I just wrote)?

@Merovius
Copy link

@Merovius Merovius commented May 6, 2022

@griesemer

Here's an attempt at a summary, slightly more compact, written down for my own understanding: […] Do I have this correctly?

Yes, I think so.

I suppose so, but I didn't see this written down. Is it written down somewhere (besides in the summary I just wrote)?

As part of the discussion, e.g. this comment. I wasn't explicit about it, but it's a consequence of the typeSet definition, which would AFAICT be the same we already use (with the exception of the "non-interface" parts).

@bcmills
Copy link
Member

@bcmills bcmills commented May 6, 2022

@griesemer

b) typeset(I): defined like in the Go spec but if I is a basic interface (no type elements), then typeset(I) also contains I

I think typeset(I) needs to include all of the interface types that implement I, not just I itself.
(That is: if a type parameter P is bounded by error, then it should be possible to instantiate P with the interface type net.Error, and thus net.Error should be in the typeset of error.)

Otherwise the instantiation rule doesn't work out.

@Merovius
Copy link

@Merovius Merovius commented May 6, 2022

Good point. This is BTW an issue with the spec right now:

The type set of a method specification is the set of types whose method sets include that method.

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.

@griesemer
Copy link
Contributor

@griesemer griesemer commented May 6, 2022

This was fixed several weeks ago. You're not looking at the spec at tip:

The type set of a method specification is the set of all non-interface types whose method sets include that method.

@bcmills
Copy link
Member

@bcmills bcmills commented May 6, 2022

@griesemer

I've been thinking about the rules in #52509 (comment) through the lens of #52624.

If we treat comparable as a run-time interface type, then we also need to define its semantics for type-assertions.

For this proposal, I believe the rule is (roughly):

  1. Assertability
    a) x asserts to i if type(x) ⊆ typeset(i)

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.

@Merovius
Copy link

@Merovius Merovius commented May 6, 2022

I think the rule is "x.(I) succeeds if x is not nil and T∈typeSet(I), where T is the dynamic type of x". Maybe you meant type(x) to be the dynamic type of x and meant typeSet(type(x)) ⊆ typeSet(I) in which case that means the same. But just to be explicit.

@Merovius
Copy link

@Merovius Merovius commented May 6, 2022

@DmitriyMV

any (as a type argument) can instantiate comparable (rule v, observation 1)

Can you provide a code sample to describe this? I'm not sure I understand it correctly.

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.
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
generics Proposal
Projects
Proposals
Incoming
Development

No branches or pull requests