フューチャー技術ブログ

Go 1.23リリース連載 range over funcとiterパッケージ

はじめに

こんにちは。CSIG 所属の棚井です。

Go 1.23 Release Notes の内容を紹介する「フューチャー技術ブログ Go 1.23 リリース連載」の記事です。

今回は2つの反復(Iterator、イテレーション)処理を取り上げます。

Release Note, Discussion, Proposal, Issue

range over funciter パッケージ のリリース内容を確認していきます。

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)
func(func(K) bool)
func(func(K, V) bool)

Relesse Note に合わせて、言語仕様にも3つのシグネチャ(func(func() bool), func(func(V) bool), func(func(K, V) bool))が追加されています(Go1.22時点での言語仕様はこちら

Range expression                                       1st value                2nd value

array or slice a [n]E, *[n]E, or []E index i int a[i] E
string s string type index i int see below rune
map m map[K]V key k K m[k] V
channel c chan E, <-chan E element e E
integer value n integer type, or untyped int value i see below
function, 0 values f func(func() bool)
function, 1 value f func(func(V) bool) value v V
function, 2 values f func(func(K, V) bool) key k K v V

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.
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
type Seq[V any] func(yield func(V) bool)

// Seq2 is an iterator over sequences of pairs of values, most commonly key-value pairs.
// When called as seq(yield), seq calls yield(k, v) for each pair (k, v) in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
type Seq2[K, V any] func(yield func(K, V) bool)
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func()) {...}

func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, 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からはオプション指定不要に

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.Rowsbufio.Scanner.Scan などで反復処理の実装方法が異なるのは「そういうものか」と暗記知識の1つとして捉えていましたが、他言語と比較すると確かに表記の統一性がないこと、Goで新しいライブラリの返り値を反復処理する場合はサンプルコードを見ないと処理方法が分からないこと、などを思い出しながら、discussion の内容を読んでいました。

利用例

本ブログで取り上げた range over funciter パッケージ の利用例を見ていきます。

環境構築

go1.23rc2 の実行環境を準備します。
golang.org/dl で該当のバージョンを選択してインストールしてください。

$ go install golang.org/dl/go1.23rc2@latest

$ go1.23rc2 download

$ go1.23rc2 version
go version go1.23rc2 linux/amd64

# 実行
$ go1.23rc2 run xxx.go

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

import (
"fmt"
)

func main() {
for range f {
fmt.Println("Hello world.")
}
}

func f(yield func() bool) {
if !yield() {
return
}
if !yield() {
return
}
}

// Output:
// Hello world.
// Hello world.

Go1.22 で追加された range over integer を利用して書き換えてみます。

func f(yield func() bool) {
- if !yield() {
- return
- }
- if !yield() {
- return
- }
+ for range 5 {
+ if !yield() {
+ return
+ }
+ }
}

// Output:
// Hello world.
// Hello world.
// Hello world.
// Hello world.
// Hello world.

func(func(K) bool)

f の中で yield() に引数を与えて、ループ処理内でその値を利用しています。

package main

import (
"fmt"
)

func main() {
for v := range f {
fmt.Println("v is: ", v)
}
}

func f(yield func(int) bool) {
for _, n := range []int{1, 2, 3, 4} {
if !yield(n) {
return
}
}
}

// Output:
// v is: 1
// v is: 2
// v is: 3
// v is: 4

func(func(K, V)bool)

func(func(K) bool) と同様に、ループ内で Key と Value を利用しています。

package main

import (
"fmt"
)

func main() {
for k, v := range f {
fmt.Printf("k is: %s, v is: %s\n", k, v)
}
}

func f(yield func(k, v string) bool) {
langs := map[string]string{
"go": "golang",
"py": "python",
"rb": "ruby",
"js": "javascript",
}
for k, v := range langs {
if !yield(k, v) {
return
}
}
}

// Output:
// k is: rb, v is: ruby
// k is: js, v is: javascript
// k is: go, v is: golang
// k is: py, v is: python

iter パッケージの利用例

反復処理(イテレータ)のパッケージとして iter が追加されたので、利用方法を見ていきます。

Go1.23 では2つの型と、2つの関数が追加されました。

// 型
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

// 関数
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())

また、range over funciter のそれぞれ追加されたシグネチャ・データ型は、以下表の対応関係があります。

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] {
return func(yield func(int) bool) {
for i := range n {
if !yield(i) {
break
}
}
}
}

func squares(n int) Seq2[int, int64] {
return func(yield func(int, int64) bool) {
for i := range n {
if !yield(i, int64(i)*int64(i)) {
break
}
}
}
}

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

import (
"fmt"
"iter"
)

func main() {
langs := map[string]string{
"go": "golang",
"py": "python",
"rb": "ruby",
"js": "javascript",
}
for k, v := range showLangs(langs) {
fmt.Printf("k is: %s, v is: %s\n", k, v)
}
}

func showLangs(langs map[string]string) iter.Seq2[string, string] {
return func(yield func(string, string) bool) {
for k, v := range langs {
if k == "go" || k == "py" {
if !yield(k, v) {
return
}
}
}
}
}

// Output:
// k is: go, v is: golang
// k is: py, v is: python

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())自体も同じ値でパニックが発生する

Pull2next() を利用して、showLangs を書き換えてみます。

package main

import (
"fmt"
"iter"
)

func main() {
langs := map[string]string{
"go": "golang",
"py": "python",
"rb": "ruby",
"js": "javascript",
}

next, _ := iter.Pull2(showLangs(langs))
for {
k, v, ok := next()
if !ok {
break
}
fmt.Printf("k is: %s, v is: %s\n", k, v)
}
}

func showLangs(langs map[string]string) iter.Seq2[string, string] {
return func(yield func(string, string) bool) {
for k, v := range langs {
if !yield(k, v) {
return
}
}
}
}

// Outpu:
// k is: go, v is: golang
// k is: py, v is: python
// k is: rb, v is: ruby
// k is: js, v is: javascript

stop() を呼び出した場合の挙動を確認します。

package main

import (
"fmt"
"iter"
)

func main() {
langs := map[string]string{
"go": "golang",
"py": "python",
"rb": "ruby",
"js": "javascript",
}

- next, _ := iter.Pull2(showLangs(langs))
+ next, stop := iter.Pull2(showLangs(langs))
for {
k, v, ok := next()
if !ok {
break
}
+ if k == "py" {
+ stop()
+ }
fmt.Printf("k is: %s, v is: %s\n", k, v)
}
}

func showLangs(langs map[string]string) iter.Seq2[string, string] {
return func(yield func(string, string) bool) {
for k, v := range langs {
if !yield(k, v) {
return
}
}
}
}

// Outpu:(mapなので実行毎に表示数は変わる)
// k is: go, v is: golang
// k is: py, v is: python

ドキュメントの記載通りに、stop() の実行後は next() から次の値が取得できないことを確認できました。

おわりに

本ブログでは、range over funciter パッケージについて、Proposal から利用例までを紹介しました。

user-defined iteration using range over func values #56413 を含めた「Goの反復処理の統合トレンド」は今後も進んでいくと思いますので、Go1.23での slicesmaps を含めて、これからのGoのアップデートが楽しみになる内容でした。

次は武田さんの unique, slices, maps です。