golangのequalityの評価について

はじめに

他の言語のつもりで比較演算子を使うと想定外の挙動をするということもあったりする。最初テキトウにコードを書いて挙動を確認しようとしていたが後に言語仕様を読めば良いだけだということに気づいた。学び始めの最中に思ったことをメモしておくというのも良いと思ったのでメモしておく。

幾つか驚いたこと

golangは比較をわりと頑張るタイプの言語のようだった。

structの比較は等値ではなく等価

初見の先入観としてstructのような値のコンテナの比較は特に何か特別なこと(e.g. 比較時に呼ばれるメソッドのオーバーライド)をしないかぎり、等値で比較されると思ったが等価だった。

例えば以下の様なこと。

type Point struct {
    x,y int
}

// 等値
pt := Point{x: 10, y: 20}
fmt.Printf("%v¥n", pt == pt) // => true

// 等価
// (先入観でこれはfalseだと思っていた)
fmt.Printf("%v¥n", Point{x: 10, y: 20}, Point{x: 10, y: 20}) // => true

値オブジェクト的な物を作った時に比較が自然に定義されるのでこのような挙動はわりと嬉しい。

また、mapのkeyの評価も同様に行われるので以下の様なコードの実行後のmapの保持する値の数は2。

m := map[Point]int{}
m[Point{X: 10, Y: 20}]++
m[Point{X: 10, Y: 20}]++
m[Point{X: 10, Y: 10}]++
fmt.Printf("%v\n", m) // => map[{10 20}:2 {10 10}:1]

mapにアクセスし値が存在しない場合にはzero値が返されるので単にcounterとして使いたい場合に便利。pythonではnamedtupleを利用した時と同様ということを考えると便利。

from collections import defaultdict

d = defaultdict(int)
d[(10, 20)] += 1
d[(10, 20)] += 1
d[(10, 10)] += 1
print(dict(d))  # => {(10, 20): 2, (10, 10): 1}

from collections import namedtuple

Point = namedtuple("Point", "x y")
d = defaultdict(int)
d[Point(x=10, y=20)] += 1
d[Point(x=10, y=20)] += 1
d[Point(x=10, y=10)] += 1
print(dict(d))  # => {Point(x=10, y=20): 2, Point(x=10, y=10): 1}


# but normal class
class Point2:
    def __init__(self, x, y):
        self.x = x
        self.y = y

d = defaultdict(int)
d[Point2(x=10, y=20)] += 1
d[Point2(x=10, y=20)] += 1
d[Point2(x=10, y=10)] += 1
print(dict(d))  # => {<__main__.Point2 object at 0x109f11828>: 1, <__main__.Point2 object at 0x109ed7550>: 1, <__main__.Point2 object at 0x109f117f0>: 1}

もっとも、namedtupleの罠として同じ形状の定義は同じになってしまうという問題があるので同じ挙動というわけではない。

Point = namedtuple("Point", "x y")
Point2 = namedtuple("Point2", "a b")
# golangではfalse
Point(10, 20) == Point2(10, 20)  # => True

slicesとmapで = を利用しようとするとコンパイルエラー

sliceとmapは = で値を比較しようとするとコンパイルエラーになる。

// slice
xs := [3]int{1, 2, 3}
i, j := xs[:], xs[:]
fmt.Printf("%v == %v, %v\n", i, j, i == j) // compile error

// map
i, j := map[string]int{"x": 1}, map[string]int{"x": 1}
fmt.Printf("%v == %v, %v\n", i, j, i == j) // compile error

後で調べてみると、mapとsliceは比較不能ただしnilとだけ比較可能という感じだった。常にfalseを返す位ならコンパイルエラーにしてしまうというのも、それはそれとして割り切りとしてありなような気がする。

以下はOK。

fmt.Printf("%v¥n", m == nil) // mに値が入っていたらfalse

map,sliceの保持する値を全部比較したければ、reflect.DeepEqual() が使える。

i, j := map[string]int{"x": 1}, map[string]int{"x": 1}
fmt.Printf("deep %v == %v, %v\n", i, j, reflect.DeepEqual(i, j))

nil同士の比較

ところで nil = nil と書いた時に値を見るだけなのか型を意識してチェックするかも気になったので以下の様なコードを書いた。これは期待通り。

type MyInterface interface{}

i, j, k := interface{}(nil), interface{}(nil), MyInterface(nil)
fmt.Printf("%v == %v, %v\n", i, j, i == j) // => true
fmt.Printf("%v == %v, %v\n", i, k, i == k) // => true

また隠した型も調べてくれるみたい。わりと便利

type Point struct {
    x, y int
}
type Point2 struct {
    x, y int
}

type MyInterface interface {
}

i, j, k := Point{x: 10, y: 20}, Point{x: 10, y: 20}, Point2{x: 10, y: 20}

// 同じ形状の値なのでtrue
fmt.Printf("%v == %v, %v\n", i, j, i == j) // i = j true

// 型が違うので比較不能
// fmt.Printf("%v == %v, %v\n", i, k, i == k) // compile error

// 型を変換すれば同じ形状の値なのでtrue
fmt.Printf("%v == %v, %v\n", i, k, i == Point(k)) // i == k true

// 同じ型にあわせて比較しても元の型が違うのでfalse
fmt.Printf("%v == %v, %v\n", i, k, MyInterface(i) == MyInterface(k)) // i == k false
fmt.Printf("%v == %v, %v\n", i, k, interface {}(i) == interface {}(k)) // i == k false

よく考えて見れば

よく考えて見れば、言語仕様を直接読めばよかった気がしないでもない。

まず、x == y についてxがyに対して代入可能(assignable)かどうか調べて、代入可能ならなんか自然な感じで型毎に比較方法が列挙されている。slices,map以外に関数も比較不能。