はじめに
Go 1.23.0
で追加された
iter
パッケージ(と
range over func
)により、
for
にメソッドを渡して反復処理を行えるようになりました。これらの使い方について紹介します。
iteratorとは
iterator
とは、配列やコレクションなどの連続したデータを順番に処理するための仕組みです。
Goにおいては、コールバック関数を受け取る関数として定義します。このコールバック関数は慣習的に
yield
と名付けられます。
イテレータ関数は以下の2つのシグネチャの内、どちらかで定義します。
-
func (yield func (v) bool)
-
func (yield func (k, v) bool)
この内、
k
と
v
はMapのKey、Valueと同様の値をとります。
また、イテレータにはpush方式とpull方式の2つの方式があります。まずはpush方式のイテレータを定義してみます。
push方式のイテレータ
まずは以下のコードをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package main import "fmt" func main() { fmt.Println("main called") for i := range myIter { fmt.Println(i) } fmt.Println("main done") } // イテレータ関数 func myIter(yield func(int) bool) { fmt.Println("myIter called") for i := range 5 { fmt.Println("myIter yield") yield(i) // 値をpushする fmt.Println("myIter yield done") } fmt.Println("myIter done") } |
このコードでは、
myIter
関数を定義して、それを for ループで実行していきます。
myIter
関数の中では、0から4の値を順番にpushしていき、それが都度 for に渡されます。
このコードを実行した際のログは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | main called myIter called myIter yield 0 myIter yield done myIter yield 1 myIter yield done myIter yield 2 myIter yield done myIter yield 3 myIter yield done myIter yield 4 myIter yield done myIter done main done |
このように、
yield
が一回実行される事に、 for の内部が一度実行されることがわかります。
kとvを受け取るイテレータ関数
引数としてkとvを受け取るタイプのイテレータ関数も定義してみます。
以下のコードをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package main func main() { for k, v := range myIter { println(k, v) } } // イテレータ関数 func myIter(yield func(k, v string) bool) { maps := map[string]string{"foo": "bar", "hoge": "fuga"} for k, v := range maps { yield(k, v) // 値をpushする } } |
先程のイテレータ関数とほとんど同じです。
このコードを実行した際のログは以下のとおりです。
1 2 | foo bar hoge fuga |
イテレータ関数をチェインさせる
イテレータ関数の応用として、イテレータ関数を受け取るイテレータ関数を定義することで、複数のイテレータ処理をチェインして処理することができます。
以下のコードをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package main import "fmt" func main() { // 1から10までの数値のうち、偶数の数値を2乗して出力する for i := range square(filter(source())) { fmt.Println(i) } } // 1から10までの数値を生成するイテレータ func source() func(func(int) bool) { // イテレータ関数を返す return func(yield func(int) bool) { for i := range 10 { yield(i + 1) } } } // 偶数のみをフィルタリングするイテレータ func filter(seq func(func(int) bool)) func(func(int) bool) { // イテレータ関数を返す return func(yield func(int) bool) { // seq関数を呼び出し、値を受け取る for i := range seq { if i%2 == 0 { yield(i) } } } } // 数値を2乗するイテレータ func square(seq func(func(int) bool)) func(func(int) bool) { // イテレータ関数を返す return func(yield func(int) bool) { // seq関数を呼び出し、値を受け取る for i := range seq { yield(i * i) } } } |
このコードでは、3つのイテレータ関数をチェインさせて実行しています。
source関数は1から10までの値を返すイテレータ関数を返します。
filter関数はそれを受け取って内部で実行し、偶数のみを返すイテレータ関数を返します。
square関数はfilter関数を受け取って内部で実行し、値を2乗して返すイテレータ関数を返します。
このコードを実行すると、1から10までの数値のうち偶数のみを2乗して出力しまう。
このコードを実行した際のログは以下のとおりです。
1 2 3 4 5 | 4 16 36 64 100 |
イテレータ関数の型を利用する
iter
パッケージには、イテレータ関数を定義する際に使える便利な方が定義されています。
1 2 | type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool) |
これを使って先ほどのコードを書き直したものがこちらになります(変更箇所のみ抜粋)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // 1から10までの数値を生成するイテレータ func source() iter.Seq[int] { // イテレータ関数を返す return func(yield func(int) bool) { for i := range 10 { yield(i + 1) } } } // 偶数のみをフィルタリングするイテレータ func filter(seq iter.Seq[int]) iter.Seq[int] { // イテレータ関数を返す return func(yield func(int) bool) { // seq関数を呼び出し、値を受け取る for i := range seq { if i%2 == 0 { yield(i) } } } } // 数値を2乗するイテレータ func square(seq iter.Seq[int]) iter.Seq[int] { // イテレータ関数を返す return func(yield func(int) bool) { // seq関数を呼び出し、値を受け取る for i := range seq { yield(i * i) } } } |
この通り、シグネチャはGenericsとして定義されているので、任意の型を渡すことができます。
イテレーションを止める
イテレーション関数を呼び出している for から break で抜けるなどした場合、このままではランタイムエラーが発生してしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package main import "fmt" func main() { for i := range myIter { if i > 5 { break } fmt.Println(i) } } // イテレータ関数 func myIter(yield func(int) bool) { for i := range 10 { yield(i) } } |
1 2 3 4 5 6 7 | 0 1 2 3 4 5 panic: runtime error: range function continued iteration after function for loop body returned false |
そこで、
yield
の返り値が
false
だった場合に for から抜けるようにすることで、エラーが発生しないようにすることができます。
1 2 3 4 5 6 7 | func myIter(yield func(int) bool) { for i := range 10 { if !yield(i) { break } } } |
pull方式のイテレータ
push方式のイテレータをpull方式に変換して実行することができます。
まずはコードをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package main import ( "fmt" "iter" ) func main() { next, stop := iter.Pull(myIter) // イテレータ関数をpull方式に変換する defer stop() // イテレータを止める for { i, ok := next() if !ok { break } fmt.Println(i) } } // イテレータ関数 func myIter(yield func(int) bool) { for i := range 10 { if !yield(i) { break } } } |
このように、
iter.Pull
関数を使ってイテレータ関数をpull方式に変換することができます。
なお、pull方式のイテレータを扱う際の注意点として、以下の2つがあげられます。
- 必ず
close
関数を呼ぶこと -
next
関数の戻り値をハンドリングすること
さいごに
Go 1.23 で登場した
iter
パッケージを使ったイテレーション関数の実装について紹介しました。