Go1.23から導入された range over func の概要と背景
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}