golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.37k stars 17.58k forks source link

proposal: spec: permit non-interface types that support == to satisfy the comparable constraint #52474

Closed ianlancetaylor closed 2 years ago

ianlancetaylor commented 2 years ago

Background

In Go 1.18 we introduced a type constraint comparable. The type constraint is satisfied by any type that supports == and for which == will not panic. For example, it is satisfied by int and string and by composite types like struct { f int } or [10]string. On the other hand, it is not satisfied by types like any or interface { String() } or struct { f any } or [10]interface{ String() }.

This decision has led to considerable discussion; for example, #50646, #51257, #51338.

When considering whether a type argument satisfies the comparable constraint, there are two cases to consider.

For an interface type, the rule in the spec is simple: an interface type T implements an interface I if "the type set of T is a subset of the type set of I." By this definition the type any does not implement comparable: there are many elements in the type set of any that are not in the type set of comparable. More generally, no basic interface implements comparable. Some general interfaces, such as interface { ~int }, implement comparable; it is not possible today to use this kind of general interface as an ordinary type, but a type parameter constrained by such a general interface will implement comparable.

For a non-interface type, the rule is different. A non-interface type T implements an interface I if it "is an element of the type set of I." For a language-defined type like comparable, the language defines that type set. The current definition says that comparable "denotes the set of all non-interface types that are comparable."

However, the current implementation is slightly different. In the current implementation, a composite type that includes an element of interface type does not implement comparable, although such a composite type is "comparable" according to the definition in the spec. The implementation was written that way based on the belief that comparable should only be implemented by types that will not panic when not used in a comparison. This is a valuable property, but it leads to some complications.

For example (this is based on a comment by @Merovius), consider a package "annotated" that implements an annotated value type:

type Value struct {
    Annotation string
    Val        any
}
// Various methods on Value.

Now consider a package "p" that uses that type, and suppose that package p ensures that it only stores values with comparable types in the Val field. It's fine for package p to use the type map[annotated.Value]bool. That works because the type annotated.Value is comparable according to the language definition. However, annotated.Value does not implement comparable, because the type of the element Val is an interface type that does not implement comparable. That means that code like this does not work:

type Set[Elem comparable] map[Elem]bool
var ValSet Set[annotated.Value]

Since annotated.Value does not implement comparable, the compiler will reject this code.

There is no good workaround for package p in this scenario. There is no way for p to say that it wants a version of the type annotated.Value that restricts Val to comparable types. It wouldn't be appropriate to change the annotated package, since that package can also be used by other packages that don't want to restrict Val to comparable types.

Proposal

Therefore, we propose that we change the implementation. Arguably the implementation is not quite following the spec here, so there may be no need to change the spec.

The new rule is:

As before:

For example, by this definition, annotated.Value implements comparable, and the problem outlined above goes away.

Note on interface types

This change means that there is little reason to seek the comparable version of a non-interface type T, as such types are now always comparable or never comparable. However, people may still want to get the comparable version of an interface type. For example, code might want the fmt.Stringer type but only permitting comparable types. If we adopt #51338, then people can get this type by writing interface { comparable; fmt.Stringer }. However, that is not part of this proposal.

Timing

Because of the confusion in this area, and the apparent discrepancy between the spec and the implementation, it may be worth implementing this in the 1.19 release, even though that is soon.

DeedleFake commented 2 years ago

So, to clarify, this would allow the following?

func Equal[T comparable](t1, t2 T) bool {
  return t1 == t2
}

type Example struct {
  Val any
}

func main() {
  Equal(Example{""}, Example{""}) // true
  Equal(Example{[]int(nil)}, Example{[]int(nil)}) // panic
}

However, Equal(any(0), any(0)) would still not be allowed, correct? And in the panicking case where it is allowed, it would just be up to the user to make sure that nothing that isn't actually comparable is used with a comparable constraint?

Without allowing any to be used directly, it would allow a workaround for the issue of CustomMap[K comparable, V any] not working as CustomMap[any, string], but it would be kind of messy because it would require that it be wrapped in a struct type first. That seems like it'll be confusing to new users.

zephyrtronium commented 2 years ago

Practically, this seems no different to me from just removing the special case that interface types are comparable types that are not in the type set of comparable. If I have a generic wrapper for sync.Map and I want the type behavior that sync.Map currently provides, I write var m Map[[1]any]any instead of var m Map[any]any, along with extra syntax around uses.

atdiar commented 2 years ago

The issue is that an interface can reside at an arbitrary deep level within a struct. (struct of struct of struct...)

Hence, it may not be obvious that a comparison may panic.

I'm not too convinced and would rather keep the current, stricter, behavior. It seems to me that this example exposes a rather rare pathological case where the comparison intent is being guaranteed by convention and not code.

On the other hand, I see the other issue: it forces the creators of packages to find out in advance whether an interface field should require comparable or not. That definitely deserves more thinking. Would be great to get such examples from the corpus of Go code that exists at large. I looked but I don't have such examples myself.

jimmyfrasche commented 2 years ago

I think ultimately the least confusing thing to allow interface values to match the comparable constraint. That doesn't stop you from allowing values of the comparable interface later. It is admittedly somewhat confusing but this is carpet stuck under furniture that's too heavy to move so it's going to stick up somewhere.

atdiar commented 2 years ago

I think ultimately the least confusing thing to allow interface values to match the comparable constraint. That doesn't stop you from allowing values of the comparable interface later. It is admittedly somewhat confusing but this is carpet stuck under furniture that's too heavy to move so it's going to stick up somewhere.

Unfortunately, I don't think that it would mix well with allowing constraint interfaces (such as comparable) as regular interfaces. That would make any interface embedding comparable only usable as a constraint. A "color" that could be more annoying to deal with.

jimmyfrasche commented 2 years ago

You can't put interface values in other interfaces so you could still allow values of the comparable interface.

atdiar commented 2 years ago

You can't put interface values in other interfaces so you could still allow values of the comparable interface.

Edit: (I had misunderstood.your response)

The issue lies with asserting an interface value to comparable or any interface embedding comparable. That would cause an incongruity. Which would also occur on variable assignment.

ianlancetaylor commented 2 years ago

@DeedleFake I agree with what you wrote above.

@DeedleFake @zephyrtronium Yes, it is easy to use a struct or array to permit using an arbitrary interface type to satisfy a comparable constraint. I think that's OK. As noted, I think that #51338 will provide a cleaner way to do the same thing while avoiding the potential panic.

@jimmyfrasche We could do that. In fact we could do that even after adopting this proposal if we decide to. I'm not personally sure that it's the least confusing approach; see the lengthy discussion in #50646 when we started down that path before changing our minds. I think it's become clear that any approach in this area is confusing.

Merovius commented 2 years ago

@atdiar

Unfortunately, I don't think that it would mix well with allowing constraint interfaces

I think it is clear that we should either allow comparable as a normal interface type or make interface-types implement comparable, but not both. They are complementary, equally simple solutions to the issues.

jimmyfrasche commented 2 years ago

@Merovius that's not at all clear to me. Doing both seems like a perfectly fine thing to do me—and the best thing to do overall. It's possible I'm missing something, though.

atdiar commented 2 years ago

@atdiar

Unfortunately, I don't think that it would mix well with allowing constraint interfaces

I think it is clear that we should either allow comparable as a normal interface type or make interface-types implement comparable, but not both. They are complementary, equally simple solutions to the issues.

Well, it also depends on whether there are plans to make all constraint interfaces usable as regular interface types (which was in consideration, I recall). The present decisions could preclude some future plans to do just that.

Merovius commented 2 years ago

@jimmyfrasche If all interface types implement comparable and comparable is a type, then

var a any = func() {}
var b comparable = a

should be allowed. In that case, I don't see the point of making comparable a type, as it does not seem to provide better guarantees than any.

To me, making comparable a type is a specific solution to guarantee that func Eq[T comparable](a, b T) bool { return a == b } never panics. To me, that goal isn't achieved if you can then instantiate Eq[any].

i.e. to me, they are both different solutions to the same problem, attacking it from opposite angles.

jimmyfrasche commented 2 years ago

You would have to write

var a any = func() {}
var b comparable = a.(comparable)

replace comparable with any other nonempty interface and your example fails at compile time and my rewrite fails at runtime so I would assume that the same would hold true for comparable.

Merovius commented 2 years ago

@ianlancetaylor

As noted, I think that #51338 will provide a cleaner way to do the same thing while avoiding the potential panic.

Personally, I think that if we make comparable a type and only comparable types assignable to that, it seems non-committal not to go all the way with that. If comparable is added, it should actually provide a guarantee that the comparison never panics. Otherwise, why even introduce it? That is, if we accept #51338, I'd be in favor of biting the bullet on making comparable viral and reject this proposal.

Merovius commented 2 years ago

@jimmyfrasche

replace comparable with any other nonempty interface and your example fails at compile time

Right, but not all interface types implement any other nonempty interface. But all interface types would implement comparable.

jimmyfrasche commented 2 years ago

If any as a type/interface satisfies comparable as a metatype/constraint, then that does not imply that the dynamic type of an interface value is necessarily comparable

ianlancetaylor commented 2 years ago

@Merovius How do you think we should address the problem with annotated.Value (which was based on one of your comments)?

The comparable constraint and interface types are a bad mix no matter what we do. I think that comparable still serves a purpose even if we adopt this proposal, though: it rejects types that are clearly not comparable, and it rejects attempts to instantiate with an interface type that may not be comparable. As @jimmyfrasche said above the carpet is going to stick up somewhere, and this proposal is suggesting a specific spot where it will stick up: composite types that contain interface types.

atdiar commented 2 years ago

There is also the option of not using a generic Set but rather an interface-based set (i.e. based on map[interface{}]bool) when one wants to be able to force comparison of potentially non-comparable values.

If applicable, then there might not be a need to special-case the generics implementation for composite types that contain interface types.

But really it depends on the cases and how widespread there are. Would be great to have real code examples.

zephyrtronium commented 2 years ago

There is also the option of not using a generic Set but rather an interface-based set (i.e. based on map[interface{}]bool) when one wants to be able to force comparison of potentially non-comparable values.

The problem isn't really with "potentially non-comparable values." There are interface types other than any which I might want to use as map keys, where what I want is to use several different concrete types in one map. reflect.Type has been a common example. ast.Node – for several different packages named ast – have been another. Since interfaces don't implement comparable, there is no way to write type-generic code for this case.

I have to duplicate the code, or at least a type-safe wrapper, for every interface type I want to use as a map key. This is exactly the problem that generics should be solving. Out of caution, for situations where comparable types are useful, it was chosen to address code duplication for all comparable types except interface types by introducing a special case for comparable. In my view, this proposal attempts to make that special case even more special and adds extra steps for actually writing the code I want to write, instead of simply making comparable consistent with the type system I've been using for eleven years.

earthboundkid commented 2 years ago

Another tact might be to allow the use of (possibly panicking) == with [T any]. Then you would only use the comparable constraint when you want assurances that no incomparables can be passed in.

leaxoy commented 2 years ago

The correct approach is redesign the type-system, not patch it, it would make type-system not self-consistent.

Comparable should be ordinary interface type, and primitive type, struct of comparable type, array of comparable type, pointer etc implement Comparable default.

Merovius commented 2 years ago

@ianlancetaylor

How do you think we should address the problem with annotated.Value (which was based on one of your comments)?

Require to duplicate the type and write custom conversions. That's a hassle, of course. But to me, that's an argument against #51338, not in favor of patching something worse over it. In my opinion, if we think we need #51338, we ought to accept this problem and see how much it comes up - at least at first.

There are two other ways (apart from this proposal) how I think we could address it, if need be:

  1. Make type-assertion on comparable "recursively" assert comparability. That is, var v annotated.Value; v.(comparable) succeeds, if the dynamic value of the Val field is comparable. This might feel a bit strange, but it makes sense in spirit of a type-assertion and I think it would keep the guarantee of comparable.
  2. Make struct types convertible, if interface fields only differ in comparabaleness, similar to how we do it with tags. That is, make annotated.Value convertible to struct{ Annotation string; Val comparable }. That would still require to duplicate the type, but at least a) you don't need a manual conversion and b) the compiler will check if the struct-definitions drift. It would still mean that the guarantee of comparable is violated, but not worse than this proposal and it would require an explicit conversion to do so.
Merovius commented 2 years ago

@carlmjohnson

Another tact might be to allow the use of (possibly panicking) == with [T any]. Then you would only use the comparable constraint when you want assurances that no incomparables can be passed in.

We forbade interface-types from implementing comparable because it was deemed unacceptable for == to panic, when using it. I don't think extending this problem to all types makes sense, then. i.e. this seems strictly worse than the original situation of having interface-types implement comparable - it extends the downsides to more situations, while also making comparable less useful.

changkun commented 2 years ago

Just a clarifying question: why it was deemed unacceptable for == to panic?

atdiar commented 2 years ago

@zephyrtronium that's a fair remark.

I guess another alternative would be to modify the concept of constraint so that it's not necessarily seen as an interface ( i.e. a constraint applicable to types belonging to a typeset) but rather a constraint applicable to any type of the type system.

So that we can constrain interfaces effectively. Typically by using predicates as constraints. (hasComparisonOperator etc.)

This would be an extension. We could still have the constraint interfaces so that we can keep comparable as a compile-time enforcer.

But effectively, there would be two kinds of constraints. One would not be "reifable" as a type and stems from the bootstrap of the type system in terms of predicates. (or maybe it could too but with different semantics, I'm not sure)

The issue to solve is that so far, we really constrain non-interface types only and we try to constrain interface types indirectly through their typesets. Just an idea.

Merovius commented 2 years ago

@atdiar We have that differentiation today. It's the difference between an interface having only methods and one which also has type elements (or is/embeds comparable).

earthboundkid commented 2 years ago

From my perspective, the current system doesn't let me write code that I want to use in practice. I have a helper Equal[T comparable](t *testing.T, want, got T). I can't do Equal(t, nil, err) because error is not comparable. I'd be happy if I could change Equal[T comparable] to Equal[T any] and just pay the price in panics if someone passes in a slice or map by mistake.

Another situation that I haven't run into yet, but I'm sure exists is maps.Keys(m) where m has an interface key type.

It's not clear to me why I should prefer not being able to use Equal and Keys, particularly as I can write the same code without generics: func EqualErr(t *testing.T, want, got error) { if want != got { ....

It also is never going to be possible to redefine error as interface{ comparable; Error() string }. So whatever changes are made, I'd prefer them to be some kind of widening that lets me use error, not a hypothetical comparableerror.

Merovius commented 2 years ago

@carlmjohnson

I'd be happy if I could change Equal[T comparable] to Equal[T any] and just pay the price in panics if someone passes in a slice or map by mistake.

You can:

func Equal[T any](t *testing.T, want, got T) {
    if any(want) != any(got) {
        t.Errorf("%v != %v", want, got)
    }
}
beoran commented 2 years ago

@changkun I think it is because == is used for nil checks which everyone uses to prevent panics. If a == nil can panick, safe nil checks become impossible.

atdiar commented 2 years ago

@atdiar We have that differentiation today. It's the difference between an interface having only methods and one which also has type elements (or is/embeds comparable).

Yes. Only difference is that those constraints are still typeset based and could be reified later. Currently, constraining interface types happens only indirectly through typesets.

Only, the typeset definition of a constraint is incongruent with the fact that interface types have the comparison operators in spite of the elements of the typeset not always having them.

It might well be the only issue where interfaces types require an extended definition of a type constraint that applies directly to them.

Merovius commented 2 years ago

@beoran I don't think that makes sense. You can't, in a function constraint on comparable, compare a type-parameter typed variable to nil - for that, you need to actually know that the underlying type can be nil. Also, comparison to nil never panics [edit] well, comparison to the pre-declared identifier nil never panics. Comparison to an interface which has dynamic value nil might. [/edit]

bcmills commented 2 years ago

@ianlancetaylor

As @jimmyfrasche said above the carpet is going to stick up somewhere, and this proposal is suggesting a specific spot where it will stick up: composite types that contain interface types.

I understand that this is a tradeoff, but I think that's a confusing and fairly subtle place to draw the line. It would mean that struct { any } is comparable where just any is not, and when folks run into problems the incentive will be to wrap problematic interfaces in otherwise-useless struct wrapper types. That will add type-declaration noise to the code, for no clear benefit.

I can understand the desire to provide static type safety and prevent avoidable run-time errors, but in most other similar tradeoffs Go biases toward conciseness over type-safety. It seems awkward to bias differently here.

In other cases we have suggested static analysis to detect failure modes that aren't covered in the formal type system. Would that be a viable alternative here? (What if we allowed all interface types to match comparable, but emitted a vet warning when passing a known-incomparable value as a comparable interface?)

beoran commented 2 years ago

This discussion is getting too complicated for me, and I think this will be even more confusing for beginners.

Let me step back a bit: in the example on top

the type annotated.Value is comparable according to the language definition. However, annotated.Value does not implement comparable

This seems to be exactly the problem. All types that are comparable according to the language definition, but only those types should also implement the comparable interface. Now what exactly is stopping us from making it so?

atdiar commented 2 years ago

@beoran in general, composite types whose components include interface types may panic when being compared.

The current implementation of the comparable constraint does not allow for such panics to occur which is valuable imho.

This proposal would relax this rule.

Still would not solve the issue that people want to use interface types in generic code. At least, it wouldn't be straightforward.

zigo101 commented 2 years ago

Maybe there is nothing needed to do here. We could just interpret any as MayBeCompared.

Set[K any, V any] and Set[K comprable, V any] may coexist and both have their respective use scenarios.

[edit]: I mean the code would be some more verbose if any is used as constraints.

type Set[K any, V any] map[any]V

func (s Set[K, V]) Put(k K, v V) {
    s[any(k)] = v
}

func (s Set[K, V]) Get(k K) (v V, ok bool) {
    v, ok = s[any(k)]
    return
}

func Equal[T any](x, y T) bool {
    return any(x) == any(y)
}

By using comparable as constraints, the code will be much cleaner.

type Set[K comparable, V any] map[any]V

func Equal[T comparable](x, y T) bool {
    return x == y
}
atdiar commented 2 years ago

The issue with any as a type constraint is that it allows the coder to pass functions, slices and maps.

There is probably a need for something in between to accomodate for interface types. But that constraint cannot define a typeset at the same time as well.

earthboundkid commented 2 years ago

any(want) != any(got) is a very neat solution to the comparability problem for Equal, but it doesn't work for set.Set[iface] or maps.Keys(ifaceMap). I think it's good to keep use cases in mind here.

Part of the problem is that the theory is misaligned with practice. Even though the spec only recognized comparable and incomparable, Go has always had three levels of comparability: 1. definitely incomparable (slices, maps), 2. definitely comparable (primitives, compounds of primitives), 3. possibly comparable (interfaces, compounds containing interfaces). Any mapping of three values onto two names is going to create mismatches. ISTM, this debate is partly about which ox to gore in the aligning. This issue proposes to treat compounds containing interfaces as comparable but not interfaces themselves, which seems like a very odd place to draw the line, since it moves some of the possibly comparable into comparable set but not all of them. Why not add a third name instead? You could loosen comparable and add strictlycomparable or keep comparable strict and add equatable (or some other, better name) for the possibly comparable.

[There's a separate problem that the definitely incomparable group actually are comparable to their zero values, but there's no way to express that comparison with generics. See #35966.]

Merovius commented 2 years ago

There's a separate problem that the definitely incomparable group actually are comparable to their zero values

That's not really true. They are comparable to the pre-declared identifier nil, not their actual zero-value.

Merovius commented 2 years ago

Sorry, I realized this must come off as picking nits. In my defense, it's vaguely relevant, as we are picking apart different notions of comparability and it demonstrates the additional nuance.

Which is why, personally, I find it so much easier to think about/teach all of this as "comparable is comparable" and consider interface types to implement comparable, but I recognize that not everyone feels that way.

earthboundkid commented 2 years ago

Since code out there already exists with [K comparable, V any, M ~map[K]V] and that code is intended to work with any valid map, my tendency is to think that comparable should be broadened to include all possibly comparable types (including interfaces), and a new name made up for "strictly comparable" types for cases where you want to guarantee non-panicking. I can be argued into other ideas though.

changkun commented 2 years ago

Why not add a third name instead?

This was proposed before in https://github.com/golang/go/issues/49587, although I found it difficult to parse and follow why the decision was converged to decline. To me, it seems just a biased decision.

jimmyfrasche commented 2 years ago

Before generics there were three kinds of comparability in Go: (copied from #26842)

There were three subsets of comparable types

Generics added a third kind of comparable to the language: recursively panic-free.

This proposal is to amend that to not directly panic free. As @bcmills notes that's effectively the same as using the old definition of comparable but with an extra step sometimes.

The choices here are ultimately to simplify what comparability means or to add more kinds of comparability.

I'd rather simplify by:

That way there is just comparable and incomparable.

The comparable constraint may not be as strict as one would like but the only subtlety would be between comparable as a type and comparable as a constraint.

@carlmjohnson would need a second helper IsZero[T any](t *testing.T, v T) and while you could write Set[any] you could also write Set[comparable].

atdiar commented 2 years ago

@carlmjohnson I like the overall idea but it's going to be my turn to be nitpicky 😂.

I think that a constraint that admits interface types should not be an interface. Because interfaces define typesets.

So I would rather keep comparable as it is but create a looser constraint for map keys and the likes.

This new constraint would be a standalone, a priori not embeddable in regular interfaces.

The universe of type constraints would include the (reifiable) constraint interfaces. Probably with a cardinality just augmented by that one new constraint.

But yes, I would rather be in favor of such an idea. Unless anyone sees a problem.

Merovius commented 2 years ago

@jimmyfrasche

  • letting all interface types satisfy the comparable type constraint
  • allowing values of the comparable interface type

I still can't really get behind having both of these. Here's another question: Should struct{ any } be assignable to comparable? If any is comparable, then all fields of that struct are comparable, so it probably should be comparable. Meanwhile, that's exactly the situation we are getting in with this proposal, which so many people find strange, where any is not assignable to comparable (at least you've argued above it shouldn't), yet struct{ any } is.

Merovius commented 2 years ago

@atdiar I'm still confused what you are trying to say. comparable already does exactly what you describe - it's a constraint, which can't be used as a type and it means (among other things) "can be used as a map key". I don't understand why you are trying to introduce a third kind of type constraint, if it does exactly the same as one of the ones we already have.

earthboundkid commented 2 years ago

I still can't really get behind having both of these. Here's another question: Should struct{ any } be assignable to comparable? If any is comparable, then all fields of that struct are comparable, so it probably should be comparable. Meanwhile, that's exactly the situation we are getting in with this proposal, which so many people find strange, where any is not assignable to comparable (at least you've argued above it shouldn't), yet struct{ any } is.

I read Jimmy as saying struct{ someInterface } would be comparable.

Not speaking for him, but I take it that the point of having var x comparable is that then you can have map[comparable]bool etc. and for that, you want strict comparability (otherwise, it's not much better than map[any]bool). OTOH, for type constraints, it seems preferable to have loose comparability. Perhaps when you write var x comparable = y, it looks at the concrete type (not interface type) of y, so the fact that y was any type is lost in the conversion. But that doesn't do much for the struct{ any } or *any case. So maybe the bite the bullet option is to say comparable is loose as a type parameter/constraint, but strict as an interface value type. I'm not sure I like that though. Then again maybe it will just be one of those little inconsistencies you never run into in practice so you never notice it.

It would be nice to be able to consolidate down to a single comparable/incomparable binary (ignoring the zero value comparisons thing for the moment), but every time someone pushes down the rug in one spot, it seems to bump up elsewhere. :-) So I'm still leaning towards a third name as the best solution so far.

Merovius commented 2 years ago

Perhaps when you write var x comparable = y, it looks at the concrete type (not interface type) of y

Do you mean the dynamic type? Becaues that's not possible, statically. The compiler can not know the dynamic type of a variable.

Then again maybe it will just be one of those little inconsistencies you never run into in practice so you never notice it.

Note that the entire discussion here is due to the fact that people, when experimenting with generics before Go 1.18, encountered that it's possible to instantiate type Set[E comparable] map[E]struct{} with an interface type and that possibly leading to a panic. So, I have little hope that this inconsistency would never get noticed.

(FTR I don't fully understand what you mean with "loose comparability". Personally, I'd prefer if we tried to largely stick with jargo which is already established. Does "loose comparability" mean "has a comparison operator which may panic"?)

It would be nice to be able to consolidate down to a single comparable/incomparable binary (ignoring the zero value comparisons thing for the moment), but every time someone pushes down the rug in one spot, it seems to bump up elsewhere

I disagree with this. To me, both of these scenarios seem entirely conflict- and IMO confusion-free:

  1. We implement #51338, interface types do not implement comparable, unless they embed comparable. Technically this is still a bit confusing, because you can compare any (because backwards compatibility), which may panic. But that's easily to detect and warn against using static tooling.
  2. We don't implement #51338, interface types do implement comparable. In this case, comparability would be the notion currently used in the spec.

ISTM inconsistencies mainly crop up if we mix these two or try to hot-patch around doing either (which is what I'd call this proposal).

jimmyfrasche commented 2 years ago

@Meroviues

Here's another question: Should struct{ any } be assignable to comparable? If any is comparable, then all fields of that struct are comparable, so it probably should be comparable.

struct { any } is a comparable type so it satisfies the comparable interface. That's not great but you could also write struct { comparable } if you want better.

Meanwhile, that's exactly the situation we are getting in with this proposal, which so many people find strange, where any is not assignable to comparable (at least you've argued above it shouldn't), yet struct{ any } is.

I don't really understand why you think that a comparable interface would behave so differently from other interfaces. comparable is a subset of any so you can't assign a value from an any to a comparable without a type assertion to check that the dynamic type is comparable. The fact that the any type statically satisfies the comparable type constraint is irrelevant.

fmt.Stringer statically satisfies the any type constraint but that doesn't mean you can write:

var a any = func() {}
var b fmt.Stringer = a
Merovius commented 2 years ago

@jimmyfrasche I was, in that particular comment, just observing that many people have above commented that it's strange for any to not be comparable, but struct{ any } to be and that that's what they didn't like about this proposal. And that your suggestion would have the same flaw, to those people. I agree that comparable should probably behave that way, under your suggestion.

[edit] though to be clear, I agree that that's a problem. If comparable is a type, I don't think struct{ any } should be assignable to it, for it to actually be useful. Which is why I don't like your suggestion - i.e. which is why I don't want to have both comparable as a type and interface types satisfying comparable [/edit]

jimmyfrasche commented 2 years ago

If generics were left as they are and the comparable interface only accepted dynamic types who were recursively panic-free wouldn't that be another solution?