Skip to content

reflect: document behaviour of DeepEqual when applied to cyclic data #20428

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

Closed
cpcallen opened this issue May 19, 2017 · 6 comments
Closed

reflect: document behaviour of DeepEqual when applied to cyclic data #20428

cpcallen opened this issue May 19, 2017 · 6 comments
Labels
Documentation Issues describing a change to documentation. FrozenDueToAge
Milestone

Comments

@cpcallen
Copy link

The documentation for reflect.DeepEqual in Go 1.8 doesn't clearly specify what happens when it is passed data containing cycles. A strict reading of the documentation might suggest that in some cases it will never terminate. Consider the following scenario:

type box struct{ n *box }
var b = &box{&box{&box{&box{nil}}}}
b.n.n.n.n = b.n.n

I will admit to naïveté in being surprised that reflect.DeepEqual(b, b.n.n) not only terminates but returns true despite b and b.n.n being structurally different: although b != b.n.n and b.n != b.n.n.n, we eventually find that b.n.n == b.n.n.n.n, so they are indeed deeply equal according to the definition given.

However, the fact that reflect.DeepEqual(b, b.n) also returns returns true is quite extraordinary and does not seem to be predictable from the definition given in the documentation: at no point do we get pointer equality, and it is not clear on what other basis it can be that "they point to deeply equal values".

(I am not proposing that the behaviour of DeepEqual be changed, even if this surprising behaviour makes it unsuitable for my particular application; I merely think the documentation should be clear about the expected behaviour.)

@gopherbot gopherbot added the Documentation Issues describing a change to documentation. label May 19, 2017
@bradfitz bradfitz added this to the Go1.9Maybe milestone May 19, 2017
@zigo101
Copy link

zigo101 commented May 22, 2017

I think it is predictable that the comparisons all return true:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	type box struct{ n *box }
	var b = &box{&box{&box{&box{nil}}}}
	b.n.n.n.n = b.n.n
	
	// all return true
	fmt.Println(reflect.DeepEqual(b, b.n))
	fmt.Println(reflect.DeepEqual(b, b.n.n))
	fmt.Println(reflect.DeepEqual(b.n, b.n.n.n))
	fmt.Println(reflect.DeepEqual(b.n.n, b.n.n.n.n))
}

But, in the following example, the two false results are not expected:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	type P *P
	var p P
	p = &p
	
	fmt.Println(p, &p, *p) // three same addresses
	fmt.Println (p == &p)                  // true
	fmt.Println(reflect.DeepEqual(p, &p))  // false
	fmt.Println (p == *p)                  // true
	fmt.Println(reflect.DeepEqual(p, *p))  // true
	fmt.Println (&p == *p)                 // true
	fmt.Println(reflect.DeepEqual(&p, *p)) // false
	
}

@cpcallen
Copy link
Author

cpcallen commented May 22, 2017

@go101:

I think it is predictable that the comparisons all return true…

For the cases where we are comparing b(.n)^i and b(.n)^i(.n)^(2j) (for i and j being non-negative integers) I agree that it is quite predictable from the docs that the comparison will return true. But for the cases where we compare b(.n)^i and b(.n)^i(.n)(2j+1) I can see no no reason to believe, based on the documentation, that DeepEqual will return true (or at all).

But, what is unexpected is the b.n != b.n.n.n

That's just because Println is not printing what you think it is. Try this:

	type box struct{ n *box }
	var b = &box{&box{&box{&box{nil}}}}
	b.n.n.n.n = b.n.n

	fmt.Printf("%p %p\n", b.n, b.n.n.n) // not the same addresses
	fmt.Println(b.n == b.n.n.n)         // false, unsurprisingly

And, in the following example, the two false results are also not expected…

They are, if you look at the types:

	type P *P
	var p P
	p = &p

	fmt.Printf("%p (%[1]T) %p (%[2]T) %p (%[3]T)\n", p, &p, *p) // three same addresses

I will admit to being a little surprised to realise that P and *P are not the same type, but upon reflection (no pun intended) that is consistent with the usual behaviour: if we say type ptrInt *int we create a new type that is distinct from its underlying type.

@zigo101
Copy link

zigo101 commented May 22, 2017

One more confused is the results printed by fmt.Println and println are different:

package main

import (
	"fmt"
)

func main() {
	type box struct{ n *box }
	var b = &box{&box{&box{&box{nil}}}}
	b.n.n.n.n = b.n.n
	
	fmt.Println(b.n, b.n.n, b.n.n.n) 
	println(b.n, b.n.n, b.n.n.n)
	// output:
	// &{0xc42000c030} &{0xc42000c038} &{0xc42000c030}
	// 0xc42000c028 0xc42000c030 0xc42000c038
}

@cpcallen,
[edit]:
Ah, I didn't notice your last comment.
Just found the outputs in this example are correct.

Yes, the types of p and &p are different, but their underlying types are the same,
that's why compiler allows comparing them.
And their values in memory are the same, so I think they should be equal.

@cpcallen
Copy link
Author

Yes, the types of p and &p are different, but their underlying types are the same,
that's why compiler allows comparing them.
And their values in memory are the same, so I think they should be equal.

The documentation for DeepEqual is very clear on this point: "Values of distinct types are never deeply equal." You might prefer the function did something different, but here the behaviour is exactly as documented, so your wish is not relevant to this issue.

@zigo101
Copy link

zigo101 commented May 22, 2017

Didn't read the DeepEqual docs carefully.
Thanks for the info. No confusions now.

@gopherbot
Copy link
Contributor

Change https://golang.org/cl/52931 mentions this issue: reflect: document how DeepEqual handles cycles

@golang golang locked and limited conversation to collaborators Aug 4, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Documentation Issues describing a change to documentation. FrozenDueToAge
Projects
None yet
Development

No branches or pull requests

4 participants