はじめに
こんにちは。CSIG 所属の棚井です。
Go 1.23 Release Notes の内容を紹介する「フューチャー技術ブログ Go 1.23 リリース連載」の記事です。
今回は2つの反復(Iterator、イテレーション)処理を取り上げます。
Release Note, Discussion, Proposal, Issue
range over func
と iter パッケージ
のリリース内容を確認していきます。
range over func がデフォルトで有効に
Go1.22 では GOEXPERIMENT
とされた range over func
が、Go1.23 からはオプション指定なしで利用可能となりました。
Go 1.23 makes the (Go 1.22) “range-over-func” experiment a part of the language. The “range” clause in a “for-range” loop now accepts iterator functions of the following types
これにより、以下3つのいずれかのシグネチャを持つ関数を、デフォルトの for-range ループ構文で反復処理できるようになりました。
func(func() bool) |
Relesse Note に合わせて、言語仕様にも3つのシグネチャ(func(func() bool)
, func(func(V) bool)
, func(func(K, V) bool)
)が追加されています(Go1.22時点での言語仕様はこちら)
Range expression 1st value 2nd value |
Range expression の追加に伴い「6.」が追記され、関数を for-range ループ処理する際の yield
の扱い方が説明されています。
6.For a function f, the iteration proceeds by calling f with a new, synthesized yield function as its argument. If yield is called before f returns, the arguments to yield become the iteration values for executing the loop body once. After each successive loop iteration, yield returns true and may be called again to continue the loop. As long as the loop body does not terminate, the “range” clause will continue to generate iteration values this way for each yield call until f returns. If the loop body terminates (such as by a break statement), yield returns false and must not be called again.
range を拡張する形で、反復処理(イテレータ)の標準化に向けた動きが進みました。
iter パッケージが追加
Go1.23 からの新しいパッケージとして iter が追加されました。
The new iter package provides the basic definitions for working with user-defined iterators.
反復処理用の構造体、及び、関数がそれぞれ2つずつ追加されています。
// Seq is an iterator over sequences of individual values. |
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func()) {...} |
Proposal から Go1.23 実装まで
Go1.23での range over func
の有効化や iter
パッケージの追加までには、複数の Discussion、Proposal、Issue が関連しています。
- discussion #54245
- discussion #56413
- issue #61405
- issue #61897
discussion #54245
discussion: standard iterator interface #54245
on 2022.8.4
- Go の 非ジェネリック処理では、
runtime.CallersFrames
,bufio.Scanner
,database/sql.Rows
の3つを例に挙げるだけでも、「反復処理の共通パターン」が無い - 上記は、Go1.18 でジェネリックが導入されるまで、反復処理用のインタフェースの記述が難しかったことが原因である
- 反復処理のインタフェースを
iter
パッケージとして追加することを提案する
discussion #56413
user-defined iteration using range over func values #56413
on 2022.10.25
- Go にはシーケンシャルなデータを反復処理する標準的な方法が無い
- 各実装ごとの個別最適により様々なアプローチを採用した結果、Goユーザのキャッチアップコストが増大してしまった
- 範囲構文(range syntax)が反復処理を直接サポートすることで、Goでの反復処理の扱いを統合していくべきである
- rangeでサポートしたい関数には、push関数とpull関数の2種類がある
issue #61405
spec: add range over int, range over func #61405
on 2023.7.18
- for-range 構文で範囲指定できる2つの新しい型の提案と追加
- Range over integer
- Go1.22で追加済み
- Range over function
- Go1.22ではオプション指定(
GOEXPERIMENT
)により利用可能 - Go1.23からはオプション指定不要に
- Go1.22ではオプション指定(
- Range over integer
issue #61897
iter: new package for iterators #61897
on 2023.8.10
iter
パッケージの提案と追加- Go1.23で追加済み
discussion #54245
の「2022.8.4」をスタート地点とした場合、Go1.23リリースの「2024.8」まで、2年越しでの iter
パッケージ追加となりました。
個人的には、discussion #56413
の以下の記述で「なるほど」との思いがありました。
In the standard library alone, we have archive/tar.Reader.Next, bufio.Reader.ReadByte, bufio.Scanner.Scan, container/ring.Ring.Do, database/sql.Rows, expvar.Do, flag.Visit, go/token.FileSet.Iterate, path/filepath.Walk, go/token.FileSet.Iterate, runtime.Frames.Next, and sync.Map.Range, hardly any of which agree on the exact details of iteration. Even the functions that agree on the signature don’t always agree about the semantics. For example, most iteration functions that return (T, bool) follow the usual Go convention of having the bool indicate whether the T is valid. In contrast, the bool returned from runtime.Frames.Next indicates whether the next call will return something valid.
When you want to iterate over something, you first have to learn how the specific code you are calling handles iteration. This lack of uniformity hinders Go’s goal of making it easy to easy to move around in a large code base. People often mention as a strength that all Go code looks about the same. That’s simply not true for code with custom iteration.
database/sql.Rows
や bufio.Scanner.Scan
などで反復処理の実装方法が異なるのは「そういうものか」と暗記知識の1つとして捉えていましたが、他言語と比較すると確かに表記の統一性がないこと、Goで新しいライブラリの返り値を反復処理する場合はサンプルコードを見ないと処理方法が分からないこと、などを思い出しながら、discussion の内容を読んでいました。
利用例
本ブログで取り上げた range over func と iter パッケージ の利用例を見ていきます。
環境構築
go1.23rc2
の実行環境を準備します。
golang.org/dl で該当のバージョンを選択してインストールしてください。
$ go install golang.org/dl/go1.23rc2@latest |
range over func の利用例
range over func
で追加された3つのシグネチャを動かしていきます。
- func(func() bool)
- func(func(K) bool)
- func(func(K, V) bool)
func(func() bool)
反復処理として range に渡す f
に対して、その関数内で yield()
が呼び出される回数だけループ処理が行われます。
package main |
Go1.22 で追加された range over integer
を利用して書き換えてみます。
func f(yield func() bool) { |
func(func(K) bool)
f
の中で yield()
に引数を与えて、ループ処理内でその値を利用しています。
package main |
func(func(K, V)bool)
func(func(K) bool)
と同様に、ループ内で Key と Value を利用しています。
package main |
iter パッケージの利用例
反復処理(イテレータ)のパッケージとして iter が追加されたので、利用方法を見ていきます。
Go1.23 では2つの型と、2つの関数が追加されました。
// 型 |
また、range over func
と iter
のそれぞれ追加されたシグネチャ・データ型は、以下表の対応関係があります。
range over func | iter |
---|---|
func(func() bool) | × |
func(func(K) bool) | Seq[V any] |
func(func(K, V) bool) | Seq2[K, V any] |
「利用方法に迷ったらテストコードを確認しよう」のルールに従い、iter/pull_test.go のコードを確認すると、以下の 2つのテスト関数 が見つかりました。range over func
と型定義の対応関係が、関数のシグネチャとして奇麗に繋がっているので参考になります。
func count(n int) Seq[int] { |
iter
の型定義により、以下のようなシグネチャの対応関係が成り立つため、count()
と squares()
の書き方が可能となります。
Seq[int]
はfunc(yield func(int) bool)
であるSeq2[int, int64]
はfunc(yield func(int, int64) bool)
である
また、Seq2[K, V any]
と func(func(K, V) bool)
の対応関係を利用すると、先ほどの func(func(K, V)bool)
の処理は、以下のような書き換えが可能です。
package main |
iter.Pull, iter.Pull2
iter
パッケージには2つの関数が追加されています。Pull2 の動作を確認してみます。
ドキュメントには以下の記載があります。
Pull2 converts the “push-style” iterator sequence seq into a “pull-style” iterator accessed by the two functions next and stop.
Next returns the next pair in the sequence and a boolean indicating whether the pair is valid. When the sequence is over, next returns a pair of zero values and false. It is valid to call next after reaching the end of the sequence or after calling stop. These calls will continue to return a pair of zero values and false.
Stop ends the iteration. It must be called when the caller is no longer interested in next values and next has not yet signaled that the sequence is over (with a false boolean return). It is valid to call stop multiple times and when next has already returned false. Typically, callers should “defer stop()”.
It is an error to call next or stop from multiple goroutines simultaneously.
If the iterator panics during a call to next (or stop), then next (or stop) itself panics with the same value.
- Pull2は
Push型
のイテレータシーケンスを、next() と stop() によりアクセス可能なPull型
のイテレータに変換する - next() はシーケンスの次のペア値と、その値が有効かを表す bool値 を返す。シーケンスが終了すると、next() はゼロ値と false のペアを返す。シーケンスの処理終了後、及び、stop() の呼び出し後にも next() は呼び出せるが、その場合は常にゼロ値と false のペアを返し続ける
- stop() は反復処理を終了する。呼び出し元がシーケンスの次の値を求めておらず、かつ、next() がまだ(false の bool値を返すことで)シーケンスの終了を通知していないときに呼び出さなければならない。stop() を複数回呼び出すこと、また、next() が既に falseを返している場合でも、stop() は呼び出し可能である。通常、呼び出し側は
defer stop()
とすべきである - 複数のゴルーチンから同時に next(), stop() を呼び出すとエラーになる
- next()(または、stop())の呼び出し中にイテレータでパニックが発生した場合、next()(または、stop())自体も同じ値でパニックが発生する
Pull2
と next()
を利用して、showLangs
を書き換えてみます。
package main |
stop()
を呼び出した場合の挙動を確認します。
package main |
ドキュメントの記載通りに、stop()
の実行後は next()
から次の値が取得できないことを確認できました。
おわりに
本ブログでは、range over func
と iter
パッケージについて、Proposal から利用例までを紹介しました。
user-defined iteration using range over func values #56413 を含めた「Goの反復処理の統合トレンド」は今後も進んでいくと思いますので、Go1.23での slices
と maps
を含めて、これからのGoのアップデートが楽しみになる内容でした。
次は武田さんの unique, slices, maps です。