Table of Contents

Go1.23 から導入された range over func の概要と背景

概要

Go1.23 から range over func が導入された。筆者は、Go 言語における反復処理の記述方法は、今後、range over func を用いた記述へと変わっていくと考えている。そのためにも、Go 言語でプログラムを書いている開発者は range over func を使えるようになっている必要がある。

本記事は range over func の基本的な使い方を述べている。range over func を使えるようになるためには、次の 2 つの概念を理解すれば十分である。それらの概念とは、range 句のイテレーター関数、yield 関数、である。

range over func とは

range 句の右側のイテレーター関数とその引数 yield

Go1.23から、for 文の range 句の右側へ関数を指定できるようになる。この関数をイテレーターと呼ぶ。参考

イテレーターのシグネチャは下記のいずれかでなければならない。

1func (yield func() bool)
2func (yield func(k) bool)
3func (yield func(k,v) bool)

イテレーターは 1 つの関数型引数(以下、便宜上 yield と呼ぶ)を持つ。

yield 関数が呼ばれると、プログラムの制御は for ブロックの 1 番上へ移る。 for 文のブロックの実行が終わる(for 文のブロックが一番下まで実行される、あるいは、break、continue、return などによって for 文のブロックを抜ける)と、プログラムの制御は yield 関数が呼ばれた直後へ戻る。

イテレーターが終了すると、for 文は反復処理を終える。

ここまでを、疑似コードにて確認してみる。

example001/main.go (注意) 疑似コードなので実行できません!

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	// for文
 7	for range seq { // seqはイテレーター
 8		// for文のブロック
 9		// yield関数の実行後、プログラムの制御はここへ移る
10		fmt.Printf("+")
11		// for文のブロックの実行後、プログラムの制御はyield関数が呼ばれた直後へ戻る。
12	}
13	fmt.Println("")
14}
15
16// イテレーター
17func seq(yield func() bool) {
18	// yield関数の実行後、プログラムの制御はfor文の1番上へ移る
19	yield()
20	// for文のブロックの実行後、プログラムの制御はここへ戻る。
21	// 以下同様
22	yield()
23	yield()
24}
1//  (注意) 疑似コードなので実際には実行できません!
2% go run cmd/example001/main.go
3+++

yield 関数が 3 回呼ばれているため、for 文のブロックが 3 回実行される。そのため + が 3 回表示されている。yield 関数が 3 回呼ばれた後、イテレーターが終了したため、for 文は反復処理を終えている。

yield 関数の引数

yield 関数の引数として値を渡した場合、この値は range 句の左側の値へコピーされる。

example002/main.go (注意) 疑似コードなので実行できません!

 1package main
 2
 3import "fmt"
 4
 5func seq(yield func(k string, v int) bool) {
 6	// yield関数の引数として値を渡す
 7	yield("hoge", 10)
 8	yield("fuga", 30)
 9	yield("foo", 20)
10}
11
12func main() {
13	for k, v := range seq {
14		// yield関数の引数として渡された値は
15		// kとvに格納されている
16		fmt.Println(k, v)
17	}
18}
1//  (注意) 疑似コードなので実際には実行できません!
2% go run cmd/example002/main.go
3hoge 10
4fuga 30
5foo 20

yield 関数の返り値

for 文のブロックの実行がどのように終わったかによって、yield 関数の返り値が変わる。

  • for 文のブロックが一番下まで実行された、あるいは、continue によって for の文のブロックを抜けた、のいずれかで場合、yield 関数の返り値は true となる。
  • break、return のいずれかによって for 文のブロックを抜けた場合、yield 関数の返り値は false となる。

yield 関数の返り値が false だった後に、yield 関数を実行すると panic が発生する。

すなわち、yield 関数の返り値が false であることは、for 文が反復処理を終えて for 文を抜けたことを意味している(for 文を抜けたにも関わらず yield 関数を呼んで for 文の反復処理を実行しようとした場合に panic が発生する)。

yield 関数の返り値が false だった場合、直ちにイテレーターを終了すること。

example003/main.go (注意) 疑似コードなので実行できません!

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6)
 7
 8func seq(yield func() bool) {
 9	for {
10		// yield関数がfalseを返した
11		// = for文を抜けた
12		// yield関数がfalseを返した後
13		// イテレーターを直ちに終了させるべき
14		if !yield() {
15			return
16		}
17	}
18}
19
20func main() {
21	start := time.Now()
22	for range seq {
23		fmt.Printf("+")
24		// 100マイクロ秒後にbreak
25		if time.Now().Sub(start) > time.Duration(100)*time.Microsecond {
26			break
27		}
28	}
29}
1//  (注意) 疑似コードなので実際には実行できません!
2% go run cmd/example003/main.go
3+++++++++++++++++++++++++++++++++++

example004/main.go (注意) 疑似コードなので実行できません!

 1package main
 2
 3func seq(yield func() bool) {
 4	yield()
 5	// yield関数がfalseを返した後で
 6	// yield関数を呼ぶとエラーとなる。
 7	yield()
 8}
 9
10func main() {
11	for range seq {
12		break
13	}
14}
1//  (注意) 疑似コードなので実際には実行できません!
2% go run cmd/example004/main.go
3panic: runtime error: range function continued iteration after function for loop body returned false
4...以下省略...

なぜ range over func が追加されたのか

Range over func をいつどう使うのか?を知るためには、なぜ Range over func が追加されたのか?を知れば良い。

For 文=反復処理

反復処理。Go 言語においては、これを for 文を用いて実現する。

for 文では、for 文のブロックが反復して実行される。反復条件の表現方法としては、条件式、range 句、の 2 種類がある。

1// 条件式が満たされていれば、for文のブロックを実行する
2for rand.Intn(2) != 1 { // 条件式
3	// for文のブロック
4}
5// range句の右側にあるスライスの要素を順番に変数vへ格納し、for文のブロックを実行する
6for i, v := range []string{"hoge", "fuga"} { // range句(RangeClause)の記述例
7	// for文のブロック
8	fmt.Println(i, v)
9}

素朴な条件式による For 文

条件式による表現はもっとも素朴(C 言語、それよりも以前からあった)である。素朴であるが故に、どのような反復処理でも記述することができるが、次の問題がある。

  • (問題 1)開発者は毎度似たような冗長な記述を書かなければならない。
  • (問題 2)人や文脈によって反復処理の記述方法が様々にある。
 1// 問題1
 2// スライスの要素を順番に1つずつアクセスしながら反復する
 3// だいたい似たような記述(iに0を代入して、iと長さを比較して・・・)
 4items := []string{"hoge", "fuga"}
 5for i := 0; i < len(items); i++ {
 6	fmt.Println(items[i])
 7}
 8histories := []int{5, 3, 6}
 9for i := 0; i < len(histories); i++ {
10	fmt.Println(histories[i])
11}
12
13// 問題2
14// その1
15items := []string{"hoge", "fuga"}
16i := 0
17for i < len(items) {
18	fmt.Println(items[i])
19	i++
20}
21
22// その2
23items := []string{"hoge", "fuga"}
24for i := 0; ; i++ {
25	if i >= len(items) {
26		break
27	}
28	fmt.Println(items[i])
29}

range 句による For 文

そこで range 句というものがある。 range 句を用いることで、よくある反復処理を簡潔に(問題 1 が解消されている!)、誰が書いても同じように(問題 2 が解消されている!)記述できるようになる。

1// スライスの要素を順番に1つずつアクセスしながら反復する
2items := []string{"hoge", "fuga"}
3for _, v := range items { // たいていは誰が書いてもこの書き方になる
4	fmt.Println(v)
5}

Go1.22 までの range 句による For 文では記述できない反復処理

Go1.22 までは、range 句の右側に指定できる型は、array、slice、map、channel、int、string だけであった。

故に、下記のような場合において、range 句では反復処理を記述できない。

  • (1)「全て」の範囲をあらかじめ決定できない
  • (2) array, slice, map で表現できる集合以外に対する反復処理

条件式による For 文を用いた反復処理を記述せざるを得ないため、(問題 1)や(問題 2)を回避できない。

(例 1)「標準入力から入力された文字列を改行区切りで 1 行ずつ処理する」プログラムは、「全て(=何行入力されるか)」をあらかじめ決定できない。従って range 句を使用できない。

 1// 問題2
 2// bufioパッケージのScannerというAPIを用いることで
 3// かなり簡潔に記述できるようになる。
 4scanner := bufio.NewScanner(os.Stdin)
 5for scanner.Scan() { // 条件式
 6	fmt.Println(">>> ", scanner.Text())
 7}
 8// bufioパッケージのReaderというAPIを用いることで
 9// Scannerと似たようなことができてしまう
10// 人によってはこちらを使うだろう
11// そして下記のfor文は冗長である(問題1)
12for {
13	line, _, err := reader.ReadLine()
14	if err != nil {
15		break
16	}
17	fmt.Println(">>> ", string(line))
18}

(例 2)「SQL データベースのテーブルからデータを取得する」プログラムは、「全て(=何行取得されるか)」をあらかじめ決定できない。従って range 句を使用できない。

 1// 一般的にはsqlパッケージのRowsというAPIを用いる
 2// 下記のコードは、(例1)の「bufioパッケージのReaderというAPIを用いた場合」とわりと似ている。
 3// 似ている理由は
 4//   「標準入力から値を取得する反復処理」
 5//   「データベースのテーブルから値を取得する反復処理」
 6// 「データ入力元」という文脈が異なるだけだから。
 7// 反復処理であるにも関わらず文脈が異なるがゆえに、記述方法が異なってしまう(問題2)。
 8for rows.Next() { // 条件式
 9	var name string
10	err = rows.Scan(&name)
11	if err != nil {
12		break
13	}
14	fmt.Println(">>> ", name)
15}

(例 3)「以下の Node 構造体によって構成された木を深さ優先探索する」プログラムは、array, slice, map で表現できる集合以外に対する反復処理である。

 1type Node struct {
 2	Data string
 3	Children []*Node
 4}
 5// 親子関係を保持しておく必要があるため、木構造を素朴なarray, slice, mapだけでは表現できない。
 6root := Node{
 7	Children: []*Node{
 8		{
 9			Data: "hoge",
10			Children: []*Node{},
11		},
12		{
13			Data: "fuga",
14			Children: []*Node{},
15		},
16		{
17			Data: "foo",
18			Children: []*Node{},
19		},
20	},
21}
22// 再起呼び出しを用いた深さ優先探索の実装。
23func dfs(node *Node, visit func(*Node)) {
24	visit(node)
25	for _, child := range node.Children {
26		dfs(child, visit)
27	}
28}
29dfs(&root, func(n *Node) {
30	// ここが反復処理。for文を用いた反復処理とはかなり違った書き方になってしまった・・・
31	fmt.Println(n.Data)
32})

Go1.23 以降の反復処理がどうなるのか

あらゆる反復処理が range over func で記述できるようになる

Go1.23 以降の range over func が目指す世界とは「全ての反復処理を range 句による For 文で記述できること」だ。

どういうことか。(例 1)から(例 3)を range over func を用いて書き換えてみよう。

(例 1)「標準入力から入力された文字列を改行区切りで 1 行ずつ処理する」プログラム

 1package main
 2
 3import (
 4	"bufio"
 5	"fmt"
 6	"io"
 7	"iter"
 8	"os"
 9)
10
11// イテレーター
12func ReadLineFromFile(
13	r io.Reader,
14) iter.Seq[string] {
15	return func(yield func(string) bool) {
16		scanner := bufio.NewScanner(r)
17		for scanner.Scan() {
18			if !yield(scanner.Text()) {
19				return
20			}
21		}
22	}
23}
24
25func main() {
26	for line := range ReadLineFromFile(os.Stdin) { // range over func
27		fmt.Println(line)
28	}
29}

(例 2)「「SQL データベースのテーブルからデータを取得する」プログラム

 1package main
 2
 3import (
 4	"database/sql"
 5	"fmt"
 6	"iter"
 7)
 8
 9type ReturnedLine struct {
10	Columns []string
11	Err     error
12}
13
14// イテレーター
15func ReadLineFromSelectQuery() iter.Seq[ReturnedLine] {
16	rows := sql.Rows{}
17	// ...
18	// Select文の発行処理(省略)
19	// ...
20	return func(yield func(ReturnedLine) bool) {
21		for rows.Next() {
22			cols, err := rows.Columns()
23			if !yield(ReturnedLine{
24				Columns: cols,
25				Err:     err,
26			}) {
27				break
28			}
29		}
30	}
31}
32
33func main() {
34	for row := range ReadLineFromSelectQuery(&rows) { // range over func
35		if row.Err != nil {
36			fmt.Println(row.Columns)
37		}
38	}
39}

(例 3)「以下の Node 構造体によって構成された木を深さ優先探索する」プログラム

 1
 2type Node struct {
 3	Data     string
 4	Children []*Node
 5}
 6
 7func dfs(node *Node, yield func(*Node) bool) {
 8	if !yield(node) {
 9		return
10	}
11	for _, child := range node.Children {
12		dfs(child, yield)
13	}
14}
15
16// イテレーター
17func all(node *Node) iter.Seq[*Node] {
18	return func(yield func(*Node) bool) {
19		dfs(node, yield)
20	}
21}
22
23func main() {
24	root := Node{
25		Data: "root",
26		Children: []*Node{
27			{
28				Data: "hoge",
29			},
30			{
31				Data: "fuga",
32				Children: []*Node{
33					{
34						Data: "bar",
35					},
36				},
37			},
38			{
39				Data: "foo",
40			},
41		},
42	}
43	for node := range all(&root) { // range over func
44		fmt.Println(node.Data)
45	}
46}

今後、反復処理を提供するライブラリは、イテレーターを反復処理のインターフェースとして提供するようになると推測する。我々は range over func の使い方を知っている必要がある。

slice や map に対する Filter や Map 関数が公式に提供されるかも?

こちらにて提案されている。果たしていつになるのか。

 1package main
 2
 3import (
 4	"fmt"
 5	"iter"
 6	"slices"
 7	"strconv"
 8)
 9
10func Filter[V any](f func(V) bool, seq iter.Seq[V]) iter.Seq[V] {
11	return func(yield func(V) bool) {
12		for v := range seq {
13			if f(v) && !yield(v) {
14				return
15			}
16		}
17	}
18}
19
20func Map[In, Out any](f func(In) Out, seq iter.Seq[In]) iter.Seq[Out] {
21	return func(yield func(Out) bool) {
22		for in := range seq {
23			if !yield(f(in)) {
24				return
25			}
26		}
27	}
28}
29
30func main() {
31	data := []int{1, 10, 31, 50}
32	seq := slices.Values(data)
33	seq = Filter(
34		func(v int) bool {
35			return v%2 == 0
36		},
37		seq,
38	)
39	seq1 := Map(
40		func(v int) string {
41			return ">>> " + strconv.Itoa(v)
42		},
43		seq,
44	)
45	for v := range seq1 {
46		fmt.Println(v)
47	}
48}