go icon indicating copy to clipboard operation
go copied to clipboard

reflect: add Value.Equal, Value.Comparable

Open ianlancetaylor opened this issue 4 years ago • 19 comments

In Go 1.17 we have introduced a conversion that can panic (#395). This is the first case in which a conversion can panic. This means that code that calls reflect.Type.ConvertibleTo and then, if that returns true, calls reflect.Value.Convert, can see an unexpected panic. (See #46730.)

Separately, for a long time now it has been possible for a comparison to panic, when comparing two interface values that have the same dynamic type but for which the dynamic type is not comparable. Therefore, for a long time code that calls reflect.Type.Comparable and then, if that returns true, uses the == operator can see an unexpected panic. (This is a fairly uncommon case as the problem only arises when working with indirectly accessed interface types, such as pointers to interfaces.)

I propose adding two new methods to reflect.Value.

// ConvertibleTo reports whether v can be converted to type t.
// If this reports true then v.Convert(t) will not panic.
func (v Value) ConvertibleTo(t Type) bool

// Comparable reports whether the type of v is comparable.
// If the type of v is an interface, this checks the dynamic type.
// If this reports true then v.Interface() == x will not panic for any x.
func (v Value) Comparable() bool

ianlancetaylor avatar Jun 14 '21 21:06 ianlancetaylor

One very minor observation:

// If this reports true then v.Interface() == x will not panic for any x.

I'm not sure this is quite right. x could be an interface containing a dynamic type that isn't comparable. In the common case, comparing two reflect.Values, you need to call Comparable on both of them. I don't have better wording to suggest.

josharian avatar Jun 14 '21 22:06 josharian

I think the statement is still true, because if the dynamic types of v.Interface() and x are different, then the comparison is false, and it doesn't matter whether either or both of the dynamic types are not comparable. In other words, if the dynamic type of v.Interface() is comparable, then either x has a different dynamic type and the result of v.Interface() == x is false, or x has the same dynamic type and the comparison will be run without panicking.

ianlancetaylor avatar Jun 14 '21 22:06 ianlancetaylor

Ah, indeed. Thanks. The relevant sentence from the spec is:

A comparison of two interface values with identical dynamic types causes a run-time panic if values of that type are not comparable.

josharian avatar Jun 14 '21 22:06 josharian

We have:

func (v Value) Addr() Value
func (v Value) CanAddr() bool

func (v Value) Interface() interface{}
func (v Value) CanInterface() bool

So it sounds like we want to add the second one of these:

func (v Value) Convert(t Type) Value
func (v Value) CanConvert(t Type) bool

And maybe:

func (v Value) Equal(u Value) bool
func (v Value) Comparable(u Value) bool

Comparable seems like a better name than CanEqual here, but Equal seems better than Compare (compare bytes.Compare, bytes.Equal).

We probably want CanConvert at least for Go 1.17.

rsc avatar Jun 16 '21 17:06 rsc

This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — rsc for the proposal review group

rsc avatar Jun 16 '21 17:06 rsc

Based on the discussion above, this proposal seems like a likely accept. — rsc for the proposal review group

rsc avatar Jul 14 '21 18:07 rsc

If a Value is Comparable, does this mean the return from Interface() can be used as a key in a map? I don't think reflect currently has a way to answer this question.

(this is just a clarifying question, not a comment against the proposal)

twmb avatar Jul 14 '21 19:07 twmb

We probably want CanConvert at least for Go 1.17.

Is this still true? The window for doing this is pretty small now.

josharian avatar Jul 14 '21 20:07 josharian

If a Value is Comparable, does this mean the return from Interface() can be used as a key in a map? I don't think reflect currently has a way to answer this question.

Assuming you have a map[interface{}]T (for some value type T), then yes: if v.Comparable() returns true, you can use v.Interface() as a key value for that map, and no panic will occur.

ianlancetaylor avatar Jul 14 '21 21:07 ianlancetaylor

Change https://golang.org/cl/334669 mentions this issue: reflect: add Value.CanConvert

gopherbot avatar Jul 14 '21 21:07 gopherbot

I sent https://golang.org/cl/334669 in case we do want this in 1.17.

ianlancetaylor avatar Jul 14 '21 21:07 ianlancetaylor

No change in consensus, so accepted. 🎉 This issue now tracks the work of implementing the proposal. — rsc for the proposal review group

rsc avatar Jul 21 '21 19:07 rsc

CanConvert has been added for 1.17. This issue remains open for Equal and Comparable in a later release.

ianlancetaylor avatar Jul 21 '21 19:07 ianlancetaylor

Change https://go.dev/cl/423794 mentions this issue: reflect: add Value.Comparable

gopherbot avatar Aug 14 '22 08:08 gopherbot

CL 423794 added both Value.{Comparable,Equal}, the issue can now be closed.

cuonglm avatar Aug 29 '22 13:08 cuonglm

Reopen for more discussion about the behavior of Value.Equal for non-comparable. At this moment, the implementation always return false, assuming the two values are valid. The point of Value.Equal is to do the equivalent of ==, so what should we do for non-comparable values? Return false? Panic?

cc @cherrymui @ianlancetaylor

cuonglm avatar Aug 30 '22 00:08 cuonglm

I think it's fine to return false.

ianlancetaylor avatar Sep 21 '22 01:09 ianlancetaylor

CC @rsc

ianlancetaylor avatar Sep 21 '22 01:09 ianlancetaylor

I think it's fine to return false.

Seems a bit counter-intuitive that:

x := reflect.ValueOf([]int{1, 2, 3})
x.Equal(x) // reports false

It's unfortunate to lose the reflexive property of equality, but I guess that's already the case for NaNs.

dsnet avatar Sep 21 '22 02:09 dsnet

@dsnet Fair point. As far as I can tell the only other option is to panic, or to change the signature of Equal.

ianlancetaylor avatar Sep 26 '22 19:09 ianlancetaylor

Given that the language itself panics for equality comparisons on incomparable types, I think it would make sense for Equal to panic for incomparable types, perhaps with a CanEqual method (akin to CanConvert, CanAddr, CanInterface, etc.) that can be used to easily avoid the panic.

bcmills avatar Sep 26 '22 19:09 bcmills

Isn't Value.CanEqual more or less identical to Value.Comparable?

dsnet avatar Sep 26 '22 19:09 dsnet

Maybe? I don't know: the proposed doc comment is “Comparable reports whether the type of v is comparable”, but for struct types whether Equal panics depends on the (deep) value, not just its (shallow) type.

bcmills avatar Sep 26 '22 20:09 bcmills

...which is why Comparable is a method on Value, not Type.

ianlancetaylor avatar Sep 26 '22 21:09 ianlancetaylor

@ianlancetaylor

which is why Comparable is a method on Value, not Type.

It is actually the inverse?

zigo101 avatar Sep 27 '22 00:09 zigo101

@go101 The Value.Comparable method is new on tip, for 1.20, for #46746.

ianlancetaylor avatar Sep 27 '22 01:09 ianlancetaylor

...which is why Comparable is a method on Value, not Type.

In that case, I think the documentation is confusing — it explicitly states that it reports (emphasis mine) “whether the type of v is comparable”, which on its face seems equivalent to v.Type().Comparable().

(The second sentence does state that “v.Interface() == x will not panic for any x”, but I would prefer that the behavior of the method be described accurately in the first sentence as well.)

bcmills avatar Sep 27 '22 12:09 bcmills

Change https://go.dev/cl/435277 mentions this issue: reflect: clarify that Value.Comparable checks the value

gopherbot avatar Sep 27 '22 16:09 gopherbot

We definitely explicitly chose above to call CanEqual Comparable instead (because it matches the language spec term).

If CanConvert returns false and you call Convert anyway, that panics. (Same for Addr, Set, Float, and so on.) So if Comparable returns false and you call Equal anyway, it seems like that should panic too.

In any other package panicking would probably not be the answer, but panicking is how reflect signals these kinds of conditions. It would be odd not to panic in this one function.

rsc avatar Sep 28 '22 17:09 rsc

This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — rsc for the proposal review group

rsc avatar Sep 28 '22 20:09 rsc