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

2024-08-14
Go

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}