emahiro/b.log

日々の勉強の記録とか育児の記録とか。

Go の iterator を触ってみた

Overview

今月にもリリースされる予定の Go1.23 に同梱されている iterator package をだいぶ今更ながら触ってみました。
どういうものか、ということの概要は知っていましたが、まぁ一旦自分でも触ってみるか、ということで触ってみて、実際動かしながら触れることで思ったことを残しておきます。

実行方法

手元は Go1.22 なので GOEXPERIMENT=rangefunc を付けて Go のファイルを実行しました。

感想

先に感想だけ書いておくと、自分としては iterator は少し直感的じゃないなという印象を持ちました。

例えばシンプルな iterator 処理を書こうと思うと以下になりますが、

func TestSeq(t *testing.T) {
    for v := range seq {
        t.Log(v)
    }
}

func seq(yeild func(int) bool) {
    for i := range 10 {
        yeild(i)
    }
}

yeild が call されるたびに for-loop の処理に一旦処理が委譲され、再度 range over func で iterator のメソッドが実行されると前回中断した yeild の処理から再開する、というのが処理の流れになりますが、そもそもメソッドを2つ行き来しないと行けないとは、直感的に読みづらいなと感じました。
自分は Typescript も業務で利用してますが map や filter がメソッド2つに分割されていたら、やっぱり読みづらいと感じてしまいます。

この辺りは書いていくうちに脳内にマップが出来上がって多少読みやすくはなるのかもしれませんが、初見では分かりづらさが勝ります。

次に yeild での for-loop への処理の委譲についてですが、以下の2つの実行結果は実は同じです。

func TestEven(t *testing.T) {
    num := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    for v := range even(num) {
        fmt.Println(v)
    }
}

func even(num []int) Seq[int] {
    return func(yield func(int) bool) {
        for _, n := range num {
            if n%2 == 0 {
                if !yield(n) {
                    break
                }
            }
        }
    }
}
func TestEven(t *testing.T) {
    num := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    for v := range even(num) {
        fmt.Println(v)
    }
}

func even(num []int) Seq[int] {
    return func(yield func(int) bool) {
        for _, n := range num {
            if n%2 == 0 {
                yield(n)
            }
        }
    }
}

どちらも2で割り切れる値のときに for-loop の処理が走ります。違うのは yeild の評価方法です。

サンプルの実装でも if !yeild(...) { return } という loop を抜けると処理を完了する、という condition を書いてる物が多かったですが、どうして否定形で書くのかいまいちわかっていませんでした。

自分なりに納得したのは例えば以下のような呼び出す側(for-loop側)が途中で loop 終わるケースで、

func TestEven(t *testing.T) {
    num := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    for v := range even(num) {
        fmt.Println(v)
        break
    }
}

iterator 側の処理を止めていない(= yeild の評価をせずそのまま yeild を実行している)ときは、

panic: runtime error: range function continued iteration after exit

goroutine 1 [running]:
main.main.func2(0x2)
        $HOME/main.go:32 +0x30
main.main.iterFilter.func3(...)
        $HOME/main.go:24
main.main()
        $HOME/main.go:32 +0x8c
exit statu

上記のような panic が発生します。これは loop はすでに抜けているのに iterator がわの処理が続いてしまうことに起因します。

こういうケースで意図しない panic を防ぐためにも yeild を評価するときは否定形で確認する実装方法がもしかしたらいいのかもしれないな、と思いました。

まとめ

何にしても Generics 以来の Go の大きなアップデートなので触りながら慣れていきたいなと思います。