Open8

Goのrangefuncを最大限悪用する試み

星にゃーん星にゃーん

Go 1.23では、特定の型の関数をfor文のrange句であたかもスライスやマップかのように使える機能(rangefunc)が追加された。

https://go.dev/play/p/PD0cAcjby70

for i := range func(yield(x int) bool) {
    yield(0)
    yield(1)
    yield(2)
} {
    fmt.Println("%d", i)
}

イテレータやジェネレータ、ストリームなどと呼ばれる類のものを実装するときに役に立つ。

星にゃーん星にゃーん

rangefuncは、おおよそ次のような読み替え規則による、関数呼び出しの糖衣構文である。

for x := fn {
  body
}
===>
fn(func(x T) bool { body })

要するに、for文の本体を関数リテラルの本体に、ループ変数を仮引数に置き換えている。

星にゃーん星にゃーん

同様の糖衣構文は様々な言語で広く普及している[1]。JavaScriptのawait文やHaskellのdo構文なども、実はrangefuncと同様の戦略による糖衣構文である。
ということはつまり、JavaScriptのawait文やHaskellのdo構文でできることが、rangefuncでもできてしまうかもしれない。

脚注
  1. 継続渡し・コールバックを読みやすくする言語機能たち(Koka・Gleam・Roc) - 星にゃーんのブログ ↩︎

星にゃーん星にゃーん

Goでファイルを読み書きする際、典型的には以下のようなコードを書く。
defer file.Close()を書き忘れるのはよくあるうっかりミスだ。
これをrangefuncでなんとかしちゃおう。

file, err := op.Open(path)
if err != nil {
    panic(err)
}
defer file.Close()

io.Copy(os.Stdout, file)
星にゃーん星にゃーん

目指すゴールは、for文の中でだけ有効なファイルハンドラreaderを作り出し、ファイルのOpenCloseを関数WithFileの中に閉じ込めること。

type Result[T any] struct {
	Ok  T
	Err error
}

func main() {
	for reader := range WithFile("main.go") {
		if reader.Err != nil {
			panic(reader.Err)
		}
		io.Copy(os.Stdout, reader.Ok)
	}
}
星にゃーん星にゃーん

WithFileの型は、iter.Seq[Result[io.Reader]]、つまりfunc(yield func(Result[io.Reader]) bool)になる。
yield関数はfor文の本体を表し、その引数はループ変数(↑の場合はreader)である。
すなわち、WithFileの実装は以下のようになる。

func WithFile(path string) iter.Seq[Result[io.Reader]] {
	return func(yield func(Result[io.Reader]) bool) {
		file, err := os.Open(path)
		if err != nil {
			yield(Result[io.Reader]{Err: err})
			return
		}

		defer file.Close()
		yield(Result[io.Reader]{Ok: file})
	}
}
星にゃーん星にゃーん

range-over functionは引数を二つ取れるので、Result型は必要なかった。

package main

import (
	"io"
	"iter"
	"os"
)

func WithFile(path string) iter.Seq2[io.Reader, error] {
	return func(yield func(io.Reader, error) bool) {
		file, err := os.Open(path)
		if err != nil {
			yield(nil, err)
			return
		}

		defer file.Close()
		yield(file, nil)
	}
}

func main() {
	for reader, err := range WithFile("main.go") {
		if err != nil {
			panic(err)
		}
		io.Copy(os.Stdout, reader)
	}
}