コンテンツにスキップ

Go/イテレータ

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go

Go言語では、イテレータは値の連続したシーケンスを走査するための強力なメカニズムです。Go 1.23で導入されたiterパッケージは、イテレータを操作するための柔軟で標準化されたアプローチを提供します。

基本概念

[編集]

イテレータとは

[編集]

Goにおけるイテレータは、シーケンスの連続する要素をコールバック関数(通常yieldと名付けられる)に渡す関数です。イテレーションは次の場合に停止します:

  • シーケンスが終了した時
  • yield関数がfalseを返し、早期停止を示した時

イテレータの種類

[編集]

Goは主に2種類のイテレータ型を定義しています:

  1. Seq[V any]: 単一の値のシーケンスをイテレートする
    • シーケンスの各値に対してyield(v)を呼び出す
    • falseを返すことで早期に停止可能
  2. 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を処理
}

高度な機能

[編集]

単一使用イテレータ

[編集]

一部のイテレータは一度だけ走査できます。これらは通常、再巻き戻しできないデータストリームから値を報告します。

変異と位置

[編集]

イテレータ自体は値の取得のみを提供し、直接の変更方法はありません。代わりに、位置型を定義し、その位置でのイテレーション中の操作を提供します。

標準ライブラリでの使用

[編集]

mapsslicesパッケージなど、いくつかの標準ライブラリパッケージがイテレータベースの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)
	}
}

このアプローチの利点:

  1. コンテキストを通じてキャンセル機能を追加
  2. メソッドチェーンを維持
  3. 外部パッケージの型に対して拡張的な操作を可能に
  4. キャンセルや他のコンテキスト機能を簡単に統合可能

コンテキストを使用することで、以下のような追加機能も簡単に実装できます:

  • キャンセル可能なイテレーション
  • タイムアウト
  • 値の伝播
  • エラー伝播

もちろん、この実装はまだ実験的なものですが、Go言語のイテレータに対する興味深いアプローチの一つと言えるでしょう。

結論

[編集]

Goのイテレータは、コレクションや値のシーケンスを柔軟かつ効率的に走査するための強力な抽象化を提供します。プッシュ型とプル型の両方のスタイルをサポートし、さまざまな反復パターンに対応できます。