Upgrade to Pro — share decks privately, control downloads, hide ads and more …

イテレータを実装するときに 知っておくと良いこと

tomato3713
September 25, 2024

イテレータを実装するときに 知っておくと良いこと

tomato3713

September 25, 2024
Tweet

More Decks by tomato3713

Other Decks in Programming

Transcript

  1. イテレータが何かを振り返る range loop に3種類の関数を渡せるようになった(range over func) 特に下2つは iter packageで iter.Seq/iter.Seq2として追加された

    The Go Programming Language Specification より 連続したデータに対して統一的な仕組みでアクセスする仕組み
  2. 単純なイテレータの例 // 果物名を順に返すイテレータ func seq(yield func(v string) bool) { yield("apple")

    yield("orange") yield("lemon") } for v := range seq { fmt.Println(v) } https://go.dev/play/p/kKcYIqvKtIh Push型での呼び出しと実行結果 yield() が呼ばれるたびにループが回る
  3. イテレータを生成して返す関数 https://go.dev/play/p/iKAlFD01hEY // min~maxまでの連番を返すイテレータ func generator(min, max int) iter.Seq[int] {

    v := min return func(yield func(v int) bool) { for v < max { yield(v) v++ } } } func main() { for v := range generator(5, 10) { fmt.Println(v) } } Push型での呼び出しと実行結果
  4. イテレータが受け取る関数リテラル • forループでの反復処理を関数リテラルにしたものが渡される ◦ yield (譲る)という名前にされることが多い ◦ 呼ばれるごとにループが進む // 果物名を順に返すイテレータ

    func seq(yield func(v string) bool) { yield("apple") yield("orange") yield("lemon") } • 引数 ◦ 反復処理で渡したい値 • 戻り値 ◦ 反復を続けるかどうか ◦ break などによって中断すると false を返す for v := range seq { fmt.Println(v) }
  5. もし途中でループが終了したら? // 数値を0から順に返すイテレータ func seq(yield func(v int) bool){ for v

    := 0; ; v++ { yield(v) } } for v := range seq { fmt.Println(v) if v > 3 { break } } • パニックする • ループ終了後もイテレータが 終了してないため
  6. パニックさせないため戻り値をハンドリングする // 数値を0から順に返すイテレータ func seq(yield func(v int) bool) { for

    v := 0; ; v++ { if !yield(v) { return } } } • ループが終了したら、早期 return する ◦ イテレータでの頻出コード yieldの戻り値は、反復を続けるかどうか • ループ継続 => true • ループ終了 => false
  7. イテレータからエラーを返したい 案 1. イテレータの 2つ目の戻り値をエラーに使う a. spec: add range over

    int, range over func · Issue #61405 · golang/go · GitHub のコメントでループ毎にエラー処理したいケースに適したやり方として提案 2. イテレータを生成する関数にエラー型変数へのポインタを渡して代入 a. spec: add range over int, range over func · Issue #61405 · golang/go · GitHub のコメントでエラー発生時にループを継続できなくなるケースに適したやり方として提案 3. 他 a. panic(err) して呼び出し側で recoverする b. error型のフィールドを持つ構造体をyield()で返す
  8. 1.イテレータの 2つ目の戻り値をエラーに使う Pros - ループごとにエラーチェックできる - 一般的なエラー処理の形に近い 特にPull型に変換した時 Cons -

    エラーに追加して返せる値が1つ以 下に限定される func seq2(yield func(v int, err error) bool) { v, err := something() if !yield(v, nil) { return } } https://go.dev/play/p/1iU0-ide6A0 https://go.dev/play/p/42m8W8Q6v_R // Push型での利用 for v, err := range seq2 { if err != nil { panic(err) } /* 何らかの処理 */ } // Pull型での利用 next, stop := iter.Pull2(seq2) v, err, ok := next() if err != nil {/* エラー処理 */} if !ok {/* 終了処理 */}
  9. 2.イテレータを生成する関数にエラー型変数へのポ インタを渡して代入 func g(retErr *error) iter.Seq[int] { return func(yield func(v

    int) bool) { v, err := something() if err != nil { *retErr = err return } if !yield(v) { return } } } func main() { var err error for v := range g(&err) { /* 何らかの処理 */ } if err != nil { panic(err) } } Pros: - 返せる値が最大の2つのまま Cons: - 一般的なエラー処理ではない - errのスコープが広い https://go.dev/play/p/NqRHKAQLsLH
  10. エラー処理の仕方を比較 1. イテレータの 2つ目の戻り値をエラーに使う a. Pros i. ループごとにエラーチェックできる ii. 一般的なエラー処理の形に近い

    b. Cons i. エラーに追加で返せる値が1つ以下に限定される 2. イテレータを生成する関数にエラー型変数へのポインタを渡して代入 a. Pros: i. 返せる値が最大の2つのまま b. Cons: i. 一般的なエラー処理ではない ii. エラー変数のスコープが広い 理由がなければ、 なじみ深い1のほうがおすすめ
  11. 実装したイテレータのテストをするときの注意 func testHelper(t *testing.T, got iter.Seq2[int, int], wants []int) {

    t.Helper() for i, s := range got { if s != wants[i] { t.Error("somthing wrong!") } } } https://go.dev/play/p/eWjUkYBXBUR 本来はここでテスト失敗と 出力されたい 実際はこっちで テスト失敗報告される • t.Helper() が意図しない動作をする ◦ 関数スタックが1段深くなってしまうため func TestA(t *testing.T) { it := slices.All([]int{1, 2, 3}) testHelper(t, it, []int{1, 2, 5}) }
  12. range-loopでイテレータがどう実行されるか for v := range seq { … } コンパイル時にrange-over-funcを使わない形に展開される

    その後、インライン展開などが行われバイナリになる ref: https://pkg.go.dev/cmd/compile/internal/r angefunc seq(func(x T) bool { ... { /* ループの中身 */ /* ループを継続しないなら */ return false } ... /* ループを継続するなら */ return true }) seq() 呼び出し に変換
  13. イテレータの全要素のテストのおすすめ func TestA(t *testing.T) { it := slices.All([]int{1, 2, 3})

    got := slices.Collect(it) want := []int{1, 2, 3} if !slices.Equal(got, want) { t.Error("somthing wrong!" ) } } • イテレータをスライスに変換 ◦ slices.Collect() • スライス同士を比較 ◦ slices.Equal() ◦ slices.EqualFunc()
  14. まとめ • イテレータを実装するとは次の3形式の関数を実装すること ◦ func(yield func() bool) ◦ func(yield func(V)

    bool) ◦ func(yield func(K, V) bool) • yield() の戻り値をハンドリングすることを忘れない ◦ ループ中断時のパニックをさけるため