Go/イテレータ
Go言語では、イテレータは値の連続したシーケンスを走査するための強力なメカニズムです。Go 1.23で導入されたiter
パッケージは、イテレータを操作するための柔軟で標準化されたアプローチを提供します。
基本概念
編集イテレータとは
編集Goにおけるイテレータは、シーケンスの連続する要素をコールバック関数(通常yield
と名付けられる)に渡す関数です。イテレーションは次の場合に停止します:
- シーケンスが終了した時
yield
関数がfalse
を返し、早期停止を示した時
イテレータの種類
編集Goは主に2種類のイテレータ型を定義しています:
Seq[V any]
: 単一の値のシーケンスをイテレートする- シーケンスの各値に対して
yield(v)
を呼び出す false
を返すことで早期に停止可能
- シーケンスの各値に対して
Seq2[K, V any]
: ペアになった値のシーケンスをイテレートする- キーと値のペアによく使用される
- シーケンスの各ペアに対して
yield(k, v)
を呼び出す
命名規則
編集コレクションのイテレータ
編集コレクション型のイテレータメソッドは、特定の命名規則に従います:
All()
: コレクション内のすべての要素を返すイテレータ- 具体的な名前で特定のシーケンスを示す:
Cities()
: 主要な都市のイテレータLanguages()
: 話される言語のイテレータ
イテレーションの順序
編集複数の反復順序が可能な場合、メソッド名はその順序を反映します:
All()
: 先頭から末尾へのイテレーションBackward()
: 末尾から先頭へのイテレーションPreorder()
: 深さ優先走査
使用例
編集基本的な範囲走査
編集func PrintAll[V any](seq iter.Seq[V]) { for v := range seq { fmt.Println(v) } }
プル型イテレータの使用
編集Pull()
関数を使用して、プッシュ型イテレータをプル型に変換できます:
next, stop := iter.Pull(seq) defer stop() for { if v, ok := next(); !ok { break } // vを処理 }
高度な機能
編集単一使用イテレータ
編集一部のイテレータは一度だけ走査できます。これらは通常、再巻き戻しできないデータストリームから値を報告します。
変異と位置
編集イテレータ自体は値の取得のみを提供し、直接の変更方法はありません。代わりに、位置型を定義し、その位置でのイテレーション中の操作を提供します。
標準ライブラリでの使用
編集maps
やslices
パッケージなど、いくつかの標準ライブラリパッケージがイテレータベースのAPIを提供しています:
// マップのキーをソートしてイテレート for _, key := range slices.Sorted(maps.Keys(m)) { // 処理 }
実践的な例:関数型イテレータの組み合わせ
編集Goのイテレータの柔軟性を示す興味深い例として、関数型イテレータの合成があります。次の例は、イテレータを関数として扱い、様々な変換や制限を簡潔に実装する方法を示しています。
サンプルコード
編集package main import ( "fmt" "iter" ) func naturals() iter.Seq[int] { return func(yield func(int) bool) { for i := 0; yield(i); i++ { } } } func evens(seq iter.Seq[int]) iter.Seq[int] { return func(yield func(int) bool) { seq(func(v int) bool { if v%2 == 0 { return yield(v) } return true }) } } func take(n int, seq iter.Seq[int]) iter.Seq[int] { return func(yield func(int) bool) { i := 0 seq(func(v int) bool { i += 1 if i <= n { return yield(v) } return false }) } } func main() { for x := range take(10, evens(naturals())) { fmt.Println(x) } }
イテレータの合成
編集この例では、3つの関数型イテレータを示しています:
naturals()
: 無限の自然数シーケンスを生成evens()
: 偶数のみをフィルタリングtake()
: 先頭のn個の要素のみを取得
これらのイテレータは関数として定義され、簡単に合成できます。take(10, evens(naturals()))
は、最初の10個の偶数を生成します。
特徴
編集この実装の利点:
- 遅延評価:シーケンス全体ではなく、必要な時だけ値を生成
- 関数型プログラミングスタイル:小さな関数を組み合わせて複雑な操作を実現
- メモリ効率:無限シーケンスも効率的に扱える
出力例
編集上記のコードは次を出力します:
0 2 4 6 8 10 12 14 16 18
この例は、Goのイテレータが関数型プログラミングのパラダイムとどのように調和するかを示しています。
メソッドチェインと遅延評価
編集概要
編集Goのイテレータは、メソッドチェインと遅延評価を通じて、非常に柔軟で効率的なシーケンス処理を可能にします。
コード例
編集package main import ( "fmt" ) // Seq は遅延評価を使用するジェネリックなシーケンス type Seq[T any] func(yield func(T) bool) // Naturals は自然数のシーケンスを生成 func Naturals() Seq[int] { return func(yield func(int) bool) { for i := 1; yield(i); i++ { } } } // Primes は素数のシーケンスを生成 func Primes() Seq[int] { return func(yield func(int) bool) { primes := []int{} for i := 2; ; i++ { flag := true for _, prime := range primes { if i%prime == 0 { flag = false break } } if flag { primes = append(primes, i) if !yield(i) { break } } } } } // Filter は条件に合う要素をフィルタリング func (seq Seq[T]) Filter(predicate func(T) bool) Seq[T] { return func(yield func(T) bool) { seq(func(v T) bool { if predicate(v) { return yield(v) } return true }) } } // Map は要素を変換する func (seq Seq[T]) Map(transform func(T) T) Seq[T] { return func(yield func(T) bool) { seq(func(v T) bool { return yield(transform(v)) }) } } // Reduce は全ての要素を畳み込む func (seq Seq[T]) Reduce(initialValue T, accumulator func(T, T) T) T { result := initialValue seq(func(v T) bool { result = accumulator(result, v) return true }) return result } // Take は指定された数だけ要素を取得する func (seq Seq[T]) Take(n int) Seq[T] { return func(yield func(T) bool) { count := 0 seq(func(v T) bool { if count < n { count++ return yield(v) } return false }) } } // Slice は全ての要素をスライスに変換する func (seq Seq[T]) Slice() []T { result := []T{} seq(func(v T) bool { result = append(result, v) return true }) return result } // メイン関数 func main() { fmt.Println("最初の10個の自然数:\t", Naturals().Take(10).Slice()) fmt.Println("最初の10個の偶数: \t", Naturals().Filter(func(x int) bool { return x%2 == 0 }).Take(10).Slice()) fmt.Println("最初の10個の素数: \t", Primes().Take(10).Slice()) fmt.Println("最初の10個の自然数の2乗:", Naturals().Take(10).Map(func(x int) int { return x * x }).Slice()) fmt.Println("最初の10個の偶数の3倍:\t", Naturals().Filter(func(x int) bool { return x%2 == 0 }).Take(10).Map(func(x int) int { return x * 3 }).Slice()) fmt.Println("最初の10個の自然数の和:\t", Naturals().Take(10).Reduce(0, func(a, b int) int { return a + b })) fmt.Println("最初の10個の自然数の積:\t", Naturals().Take(10).Reduce(1, func(a, b int) int { return a * b })) }
遅延評価の利点
編集無限シーケンスの処理
編集従来の配列やスライスと異なり、このイテレータの実装は「無限」シーケンスを効率的に扱えます:
Naturals()
は理論上無限の自然数を生成Primes()
は理論上無限の素数を生成- メモリ全体を一度に確保するのではなく
- 必要な時にのみ値を生成
- 必要な数の要素だけを計算
メソッドチェインの特徴
編集Naturals().Filter(func(x int) bool { return x%2 == 0 }).Take(10).slice()
は次のように動作します:
- 自然数のシーケンスを生成
- その中から偶数のみを選択
- 先頭の10個の要素のみを取得
- シーケンスをイテレータに変換
このアプローチの利点:
- コードが読みやすい
- 各変換が独立している
- メモリ効率が高い
- 計算コストを最小限に抑える
実行結果
編集最初の10個の自然数: [1 2 3 4 5 6 7 8 9 10] 最初の10個の偶数: [2 4 6 8 10 12 14 16 18 20] 最初の10個の素数: [2 3 5 7 11 13 17 19 23 29] 最初の10個の自然数の2乗: [1 4 9 16 25 36 49 64 81 100] 最初の10個の偶数の3倍: [6 12 18 24 30 36 42 48 54 60] 最初の10個の自然数の和: 55 最初の10個の自然数の積: 3628800
高度な使用例
編集このパターンは、以下のような複雑な変換にも応用可能です:
- フィルタリング
- マッピング
- 要素の制限
- 複雑な条件での選択
技術的な洞察
編集- 型パラメータ
[T any]
による汎用性 - ジェネリックプログラミングの活用
- 関数型プログラミングの原則の実践
この実装は、Goにおけるイテレータの柔軟性と表現力を示す優れた例です。遅延評価、メソッドチェイン、ジェネリクスを組み合わせることで、強力で効率的なシーケンス処理が可能になります。
コンテキストを使用したメソッドチェイン
編集コンテキストを利用したアプローチは興味深い解決策になる可能性があります。以下のような実装が考えられます:
package main import ( "context" "fmt" "iter" ) // コンテキストを使用したSeqラッパー func WithSeq[T any](ctx context.Context, seq iter.Seq[T]) SeqContext[T] { return SeqContext[T]{ ctx: ctx, seq: seq, } } type SeqContext[T any] struct { ctx context.Context seq iter.Seq[T] } func (s SeqContext[T]) Evens() SeqContext[T] { return SeqContext[T]{ ctx: s.ctx, seq: func(yield func(T) bool) { s.seq(func(v T) bool { if x, ok := any(v).(int); !ok { return true } else if x%2 == 0 { select { case <-s.ctx.Done(): return false default: return yield(v) } } return true }) }, } } func (s SeqContext[T]) Take(n T) SeqContext[T] { return SeqContext[T]{ ctx: s.ctx, seq: func(yield func(T) bool) { i := 0 m, ok := any(n).(int) s.seq(func(v T) bool { select { case <-s.ctx.Done(): return false default: if !ok { return false } i += 1 if i <= m { return yield(v) } return false } }) }, } } func (s SeqContext[T]) Range() iter.Seq[T] { return s.seq } func naturals() iter.Seq[int] { return func(yield func(int) bool) { for i := 0; yield(i); i++ { } } } func main() { // キャンセル可能なコンテキストを使用 ctx, cancel := context.WithCancel(context.Background()) defer cancel() // メソッドチェーンでシーケンス処理 for x := range WithSeq(ctx, naturals()).Evens().Take(10).Range() { fmt.Println(x) } }
このアプローチの利点:
- コンテキストを通じてキャンセル機能を追加
- メソッドチェーンを維持
- 外部パッケージの型に対して拡張的な操作を可能に
- キャンセルや他のコンテキスト機能を簡単に統合可能
コンテキストを使用することで、以下のような追加機能も簡単に実装できます:
- キャンセル可能なイテレーション
- タイムアウト
- 値の伝播
- エラー伝播
もちろん、この実装はまだ実験的なものですが、Go言語のイテレータに対する興味深いアプローチの一つと言えるでしょう。
結論
編集Goのイテレータは、コレクションや値のシーケンスを柔軟かつ効率的に走査するための強力な抽象化を提供します。プッシュ型とプル型の両方のスタイルをサポートし、さまざまな反復パターンに対応できます。