焼売飯店

GoとかTS、JSとか

Go Quiz Advent Calendar【17日目】 - メソッド値は難しかった編

こちらは Goクイズ Advent Calendar 2020 - Qiita の17日目の記事です。


問題

今回はMethod values (メソッド値) の問題です。(前回のGo Language Specification輪読会で見付けたものです)

package main

import "fmt"

type I interface {
    M() int
}

type T struct {
    a int
}

func (t T) M() int {
    return t.a
}

func main() {
    var t *T = &T{a: 1}
    f1 := t.M

    var i I
    i = t
    f2 := i.M

    t.a = 2

    fmt.Println(f1(), f2())
}

f1(), f2() の結果を問う、引っ掛け無しの問題となります。 さて、答えはどれでしょう?

  1. 1 1
  2. 1 2
  3. 2 2

解答

https://play.golang.org/p/1tHE-qcsNtK

正解

スクロールした先にあります!

正解は、2の 1 2 です。

解説

この問題を解くためには、メソッド値のレシーバに設定される値がどのように定まるかを理解する必要があります。

先にまとめると、

  • メソッド値のレシーバには、メソッド値を評価した時点での対象の式の値がコピーされる
  • メソッド値のレシーバのコピーの挙動は、通常の変数のコピーの挙動と変わらない (と思われる)

と言う2点が気にすべきポイントとなります。

そもそもメソッド値とは?

メソッド値は、 x.M のような形式で、静的型を持つ式 x のメソッド M を参照した時に得られる値です。 具体例を示します。

次のような型 MyInt と、そのメソッド Double があるとします。 このメソッド Double は、単純にレシーバ自身を2倍して、その結果をint型で返します。

type MyInt int

func (i MyInt) Double() int {
    return int(i * 2)
}

この時、次のように MyInt 型の変数 a, b を宣言し、それぞれ値に 2, 3 を設定します。

var (
    a MyInt = 2
    b MyInt = 3
)

変数 a, b のメソッド Double のメソッド値を、変数 fA, fB に設定して実行すると、それぞれ 4, 6 と言う値が得られます。

fA := a.Double
fmt.Println(fA()) // 4

fB := b.Double
fmt.Println(fB()) // 6

https://play.golang.org/p/uFslCHl1Ifn

変数 a, b に設定していた値を使って計算が行われていることから、メソッド値は メソッド値を得た時点でのレシーバの値をコピーして使用している ことがわかります。

レシーバの値のコピーの性質について

先ほどと同じ MyInt 型と Double メソッドを利用した例です。

ここでは、同一の変数 a の値を変更してメソッド値を2回取得しています。 1度目はaに 2 が代入された状態で、2度目はaに 3 が代入された状態となっています。

var a MyInt = 2

f1 := a.Double

a = 3 // 値を更新する

f2 := a.Double

fmt.Println(f1()) // 4
fmt.Println(f2()) // 6

結果として、f1, f2は、同一の変数から取得したメソッド値であるにも関わらず、実行結果が異なっていました。

これは、先ほど述べた通り メソッド値を得た時点でのレシーバの値をコピーしている からです。

Specにも同様の内容が書かれています。 上記の例で言う a.Doublea が、Specに書かれている The expression x に相当します。

The expression x is evaluated and saved during the evaluation of the method value; the saved copy is then used as the receiver in any calls, which may be executed later.

下記の例のように、レシーバがポインタ型だった場合は値の変更がメソッド値にも反映されます。

type S struct {
    N int
}

func (s *S) Double() int { // レシーバ s はポインタ型
    return s.N * 2
}

func main() {
    var a *S = &S{ // a はポインタ型
        N: 2,
    }

    f1 := a.Double

    a.N = 3 // 値を更新する

    f2 := a.Double

    fmt.Println(f1()) // 6
    fmt.Println(f2()) // 6
}

https://play.golang.org/p/LYlufT7i4gO

変数がポインタ型でない場合、メソッドの呼び出しで & を省略できるのと同じルールがメソッド値でも適用されます。

type S struct {
    N int
}

func (s *S) Double() int { // レシーバ s はポインタ型
    return s.N * 2
}

func main() {
    var a S = S{ // a はポインタ型でない
        N: 2,
    }

    f1 := a.Double // (&a).Double と書いているのと同じ

    a.N = 3 // 値を更新する

    f2 := a.Double // (&a).Double と書いているのと同じ

    fmt.Println(f1()) // 6
    fmt.Println(f2()) // 6
}

https://play.golang.org/p/FMdoj2e8YSP

次のように、変数がポインタ型で、レシーバがポインタ型でない場合、自動で変数をdereferenceした結果がレシーバとしてコピーされるため、結果が 4, 6 となります。

これはこうでないと困りますが、非常にややこしい挙動ですね…。

type S struct {
    N int
}

func (s S) Double() int { // レシーバ s はポインタ型でない
    return s.N * 2
}

func main() {
    var a *S = &S{ // a はポインタ型
        N: 2,
    }

    f1 := a.Double

    a.N = 3 // 値を更新する

    f2 := a.Double

    fmt.Println(f1()) // 4
    fmt.Println(f2()) // 6
}

https://play.golang.org/p/R9CN5BbJLXF

インタフェース型の変数のメソッド値について

メソッド値は、インタフェース型の変数からも取得することができます。

この時の値のコピーのルールがややこしく、難解な部分です。

Specには、次のように書かれています。

Although the examples above use non-interface types, it is also legal to create a method value from a value of interface type.

インタフェース型からも、非インタフェース型と同じようにメソッド値を取得できるとしか書かれていません。

もっともシンプルな例から見てみましょう。

初めに提示した MyInt に対応する Doubler interfaceを定義して、メソッド値を取得しています。

ここでの挙動は、先ほどの例と完全に同じで結果が 4, 6 となります。インタフェース型になっても、値がコピーされる挙動は変わりません。

type Doubler interface {
    Double() int
}

type MyInt int

func (i MyInt) Double() int {
    return int(i * 2)
}

func main() {
    var a Doubler = MyInt(2)

    f1 := a.Double

    a = MyInt(3) // 値を更新する

    f2 := a.Double

    fmt.Println(f1()) // 4
    fmt.Println(f2()) // 6
}

https://play.golang.org/p/Pm-I6kTwU3w

続いて、変数の動的型がポインタ型の例を見てみます。 これも直感的で、結果は 6, 6 となります。

type Doubler interface {
    Double() int
}

type S struct {
    N int
}

func (s *S) Double() int {
    return s.N * 2
}

func main() {
    var s = &S{ // s はポインタ型 *S
        N: 2,
    }
    var a Doubler = s // s の静的型は Doubler, 動的型は *S

    f1 := a.Double

    s.N = 3 // 値を更新する

    f2 := a.Double

    fmt.Println(f1()) // 6
    fmt.Println(f2()) // 6
}

https://play.golang.org/p/BvZD-mBGuZ1

変数の動的型がポインタ型でない場合は、interfaceを実装していない扱いとなりコンパイルが通りません。

変数 s に対して s.Double() を呼ぶことは出来ますが、これはあくまで (&s).Double() の省略記法でしかなく、型Sのメソッドセットに Double が含まれていないからです。(詳しくはMethod setsの説明等を参照ください)

type Doubler interface {
    Double() int
}

type S struct {
    N int
}

func (s *S) Double() int {
    return s.N * 2
}

func main() {
    var s = S{ // s はポインタ型でない
        N: 2,
    }
    var a Doubler = s // compile error (型Sは、Doublerを実装していない)

https://play.golang.org/p/hcqF3aGNh-a

次が最後の難関です。

変数の動的型がポインタ型の場合、先ほどと異なり、結果は 6, 6 となります。

レシーバの型は先ほどと同じ S であるにも関わらず、自動的に変数のdereferenceを行って値をコピーする挙動は発生しないようです。

type Doubler interface {
    Double() int
}

type S struct {
    N int
}

func (s S) Double() int {
    return s.N * 2
}

func main() {
    var s = &S{ // s はポインタ型
        N: 2,
    }
    var a Doubler = s

    f1 := a.Double

    s.N = 3 // 値を更新する

    f2 := a.Double

    fmt.Println(f1()) // 6
    fmt.Println(f2()) // 6
}

この辺りで訳がわからなくなってくるので、もう少し丁寧に見てみましょう。

最後の難関

問題の箇所を抜き出して、比較しやすくした例です。

元の変数を v 、インタフェース型の変数を i としています。

v.N を書き換えた時に、全く同じ挙動をするように見えるメソッド値 fv1, fi1 の結果は異なっていました。

type Doubler interface {
    Double() int
}

type S struct {
    N int
}

func (s S) Double() int {
    return s.N * 2
}

func main() {
    var v = &S{ // s はポインタ型
        N: 2,
    }
    var i Doubler = v

    fv1 := v.Double
    fi1 := i.Double

    v.N = 3 // 値を更新する

    fv2 := v.Double
    fi2 := i.Double

    fmt.Println(fv1()) // 4
    fmt.Println(fi1()) // 6
 
    fmt.Println(fv2()) // 6
    fmt.Println(fi2()) // 6
}

https://play.golang.org/p/R4k1ZPehCHW

ここで、Specに メソッド値の取得時に式を評価した結果が保存される ことが書かれていたのを思い出してみると、実はこの例は下記のように書くことも出来るのではないかと思い当たりました。

func main() {
    var v1 = &S{ // s はポインタ型
        N: 2,
    }
    var i1 Doubler = v1

    v2 := v1 // v1のコピーを作る
    i2 := i1  // i1のコピーを作る

    fv1 := v1.Double
    fi1 := i1.Double
 
    v1.N = 3 // 値を更新する
 
    fv2 := v2.Double
    fi2 := i2.Double

    fmt.Println(fv1()) // 4
    fmt.Println(fi1()) // 6
 
    fmt.Println(fv2()) // 6
    fmt.Println(fi2()) // 6
}

https://play.golang.org/p/KYgciQm-i8D

v, iv1, i1 として、それをそのままコピーした v2, i2 を作り、そこから取得したメソッド値を fv2, fi2 として、先ほどと同じ内容を実行して、同じ結果が得られました。

かなり遠回りした感じがありますが、恐らく メソッド値のレシーバのコピーの挙動は、通常の変数のコピーの挙動と変わらない です。

これを意識した上でコードを読むように注意すれば、今回の問題のような挙動も読み解けるのではないかと思います。

とは言えこれはかなりしんどいので、インタフェース型の変数とメソッド値を組み合わせて使う場合は、難解な挙動があることを気を付けた上で使うようにしてみてください。

問題の解説はSkipしますが、ここまでの情報をヒントに読み直してみてください!