任意のイテレータを作る

Go でイテレータを組む機会があったので,思い出しつつ,簡単なメモを残しておく。 なお Go のイテレータは 1.23 から導入されている。

たとえば配列(または slice)や連想配列(map)であれば for-range 構文で以下のように書ける。

package main

func main() {
	nums := []int{1, 2, 3}
	for _, num := range nums {
		println(num)
	}

	planets := map[string]float64{
		"Mercury": 0.055,
		"Venus":   0.815,
		"Earth":   1.0,
	}
	for planet, mass := range planets {
		println(planet, ":", mass)
	}
}

配列や連想配列以外でも for-range 構文が使えれば何かと便利だろう。 Go ではこれを range over function という仕組みで実現している。

Range Over Functions

具体的には以下の3つの関数型を使う。

The improved for/range statement doesn’t support arbitrary function types. As of Go 1.23 it now supports ranging over functions that take a single argument. The single argument must itself be a function that takes zero to two arguments and returns a bool; by convention, we call it the yield function.

func(yield func() bool)
func(yield func(V) bool)
func(yield func(K, V) bool)

例として,3つの乱数を返すイテレータを作ってみよう。 こんな感じかな。

package main

import "math/rand/v2"

func randoms(count int) func(func(int) bool) {
	return func(yield func(int) bool) {
		for range count { // range over int (from 0 to count-1)
			if !yield(rand.Int()) {
				return
			}
		}
	}
}

func main() {
	for n := range randoms(3) {
		println(n)
	}
}

強調部分がイテレータの本体である。 これを実行すると,以下のような出力になる。

$ go run sample1a.go
27723168966634698
2343875658410196265
4739277610031835691

このように yield() 関数を呼び出した回数だけ for-range ループが回っていることが分かる。 yield() 関数は何をしているかというと,引数の値をループに push して,処理を継続する場合は true を返している。

ちなみに yield() 関数の返り値のチェックをサボるとどうなるかというと,たとえば

package main

import "math/rand/v2"

func randoms2(count int) func(func(int, int) bool) {
	return func(yield func(int, int) bool) {
		for i := range count { // range over int (from 0 to count-1)
			yield(i+1, rand.Int())
		}
	}
}

func main() {
	for i, n := range randoms2(3) {
		println(i, n)
		if i >= 2 {
			break
		}
	}
}

のように,3回出力するイテレータに対して何らかの理由で2回で中断したとすると

$ go run sample1b.go
1 5146054532611686492
2 7588979459646265126
panic: runtime error: range function continued iteration after function for loop body returned false

goroutine 1 [running]:
main.main-range1(...)
	/path/to/sample1b.go:16
main.main.randoms2.func1(...)
	/path/to/sample1b.go:10
main.main()
	/path/to/sample1b.go:16 +0xcd
exit status 2

などと panic を吐く。 怖いですねぇ(笑)

試しに上のコードの randoms2() 関数を

func randoms2(count int) func(func(int, int) bool) {
	return func(yield func(int, int) bool) {
		for i := range count { // range over int (from 0 to count-1)
			println("calling", i+1)
			if !yield(i+1, rand.Int()) {
				println("return false from yield")
				return
			}
		}
	}
}

などとデバッグ文を入れて書き直すと

$ go run sample1c.go
calling 1
1 682324887426579321
calling 2
2 5758210549060056534
return false from yield

のように出力される。 yield() 関数の呼び出しは2回までで,2回目に false を返していることが分かるだろう。

iter パッケージ

イテレータを比較的簡単に取り扱えるようにするため iter パッケージが用意されていて,イテレータを以下のように定義している。

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

前節の randoms() 関数であれば

import (
	"iter"
	"math/rand/v2"
)

func randoms(count int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for range count { // range over int (from 0 to count-1)
			if !yield(rand.Int()) {
				return
			}
		}
	}
}

のように書き直せる。 ん? 大して変わらんな(笑)

もうひとつ。 iter パッケージには iter.Pull() および iter.Pull2() 関数が用意されていて,たとえば先程の randoms() 関数の呼び出し側で,以下のように,push-style から pull-style に変形できる。

func main() {
	next, stop := iter.Pull(randoms(3))
	defer stop()
	for {
		n, ok := next()
		if !ok {
			break
		}
		println(n)
	}
}

これはアリだろう。

slices パッケージ

slices パッケージにもイテレータ関連の関数が追加された。 たとえば All() 関数を使って

package main

import "slices"

func main() {
	nums := []int{1, 2, 3}
	for i, num := range slices.All(nums) {
		println(i, ":", num)
	}
}

のように slice をイテレータに変換する。 記述が増えただけで見た目は変わらんやんけ! と思うかも知れないが

package main

import (
	"iter"
	"slices"
)

func main() {
	nums := []int{1, 2, 3}
	next, stop := iter.Pull(slices.Values(nums))
	defer stop()
	for {
		num, ok := next()
		if !ok {
			break
		}
		println(num)
	}
}

のように pull-style に変形できるのはいいかもしれない。 また,イテレータに変換する際に Backward() 関数で逆順に並び替えたり Chunk() 関数で slice を分割することもできる。

Collect() 関数を使ってイテレータを slice に変換することもできる。 前節の randoms() 関数であれば

func main() {
	randlist := slices.Collect(randoms(3))
	fmt.Println("randlist:", randlist)
}

のように書く。 また AppendSeq() 関数で既にある slice インスタンスにイテレータの出力を追加したり,Sorted() 関数等を使った並び替えもできる。

maps パッケージ

maps パッケージにもイテレータ関連の関数が追加された。 ここでは簡単に関数名だけ紹介する。

  • All : map 型を iter.Seq2 型に変換する
  • Keys : map 型のキーを iter.Seq 型に変換する
  • Values : map 型の値を iter.Seq 型に変換する
  • Collect : iter.Seq2 型を map 型に変換する
  • Insert : 既にある map インスタンスに iter.Seq2 型の出力を追加する

改訂版: 土用期間中の干支を数え上げる

以前書いた「第五の季節:土用」という記事で,拙作の goark/koyomi パッケージを使って,土用期間中の干支を数え上げるコードを紹介した。 こんな感じ。

package main

import (
	"fmt"
	"time"

	"github.com/goark/koyomi/value"
	"github.com/goark/koyomi/zodiac"
)

func main() {
	start := value.NewDateYMD(2026, time.July, 20)   //土用の入り
	end := value.NewDateYMD(2026, time.August, 7)    //立秋
	for d := start; d.Before(end); d = d.AddDay(1) { //1日単位で立秋の前日まで出力する
		,  := zodiac.ZodiacDayNumber(d)
		fmt.Printf("Day %v is %v%v\n", d, , )
	}
}

実行結果は以下の通り。

$ go run sample6a.go
Day 2026-07-20 is 乙未
Day 2026-07-21 is 丙申
Day 2026-07-22 is 丁酉
Day 2026-07-23 is 戊戌
Day 2026-07-24 is 己亥
Day 2026-07-25 is 庚子
Day 2026-07-26 is 辛丑
Day 2026-07-27 is 壬寅
Day 2026-07-28 is 癸卯
Day 2026-07-29 is 甲辰
Day 2026-07-30 is 乙巳
Day 2026-07-31 is 丙午
Day 2026-08-01 is 丁未
Day 2026-08-02 is 戊申
Day 2026-08-03 is 己酉
Day 2026-08-04 is 庚戌
Day 2026-08-05 is 辛亥
Day 2026-08-06 is 壬子

次の土用の丑の日は7月26日か。

それはともかく,コード中の for 文がカッコ悪いなぁと以前から思ってたので,今回の勉強ついでに,指定期間の日付を出力するイテレータを実装してみた。 これを使うと,こんな感じに書き直せる。

package main

import (
	"fmt"
	"time"

	"github.com/goark/koyomi/value"
	"github.com/goark/koyomi/zodiac"
)

func main() {
	seq, err := value.NewDateYMD(2026, time.July, 20).IterDay( //土用の入りから
		1, //1日単位で
		value.NewDateYMD(2026, time.August, 7).AddDay(-1), //立秋の前日まで出力する
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	for d := range seq {
		,  := zodiac.ZodiacDayNumber(d)
		fmt.Printf("Day %v is %v%v\n", d, , )
	}
}

どうよ。 こっちのほうが少し見やすいべ。 Before() とか After() とか直感的に分かりにくいメソッドが隠蔽できるだけでもだいぶありがたい。

ブックマーク

参考図書

photo
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
丸善出版 2016-06-20
単行本(ソフトカバー)
4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN)
評価     

著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。と思ったら絶版状態らしい(2025-01 現在)。復刊を望む!

reviewed by Spiegel on 2016-07-13 (powered by PA-APIv5)

photo
効率的なGo ―データ指向によるGoアプリケーションの性能最適化
Bartłomiej Płotka (著), 山口 能迪 (翻訳)
オライリー・ジャパン 2024-02-24
単行本(ソフトカバー)
4814400535 (ASIN), 9784814400539 (EAN), 4814400535 (ISBN)
評価     

版元で Ebook を買える。Go言語のリファレンス本ではない。フトウェア工学,プログラミング(の考え方)を学ぶ教科書的な位置づけかなぁ。

reviewed by Spiegel on 2024-04-21 (powered by PA-APIv5)

photo
Go言語で学ぶ並行プログラミング 他言語にも適用できる原則とベストプラクティス impress top gearシリーズ
James Cutajar (著), 柴田 芳樹 (著)
インプレス 2024-12-04 (Release 2024-12-04)
Kindle版
B0DNYMMBBQ (ASIN)
評価     

読書会のために購入。インプレス社の本は Kindle 版より版元で PDF 版を買うのがオススメ。「並行処理」について原理的な解説から丁寧に書かれている。 Go で解説されているが Go 以外の言語でも応用できる。

reviewed by Spiegel on 2025-01-25 (powered by PA-APIv5)