🐹

Go1.22 の実験的機能 rangefunc で遊んでみた

2024/02/17に公開

Go 1.22 で実験的機能として rangefunc が試せるようになっています。

今日はちょっとコイツで遊んでみようと思います。

まずは使ってみよう

環境変数 GOEXPERIMENT=rangefunc をセットすると使えるようになります。

というわけで、 VSCode にチョチョイと設定を入れてやって試しましょう。

.vscode/settings.json
{
    "go.toolsEnvVars": {
        "GOEXPERIMENT": "rangefunc"
    }
}

動かしてみたいだけですからね、すごい単純なやつでいいです。
たとえばこんなの

rangefn_test.go
package go1_22_rangefn_test

import "fmt"

type Iterator[T any] func(func(T) bool)

func Iter[T any](values []T) Iterator[T] {
	return func(yield func(T) bool) {
		for _, t := range values {
			yield(t)
		}
	}
}

func Example() {
	for t := range Iter([]int{1, 2, 3, 4, 5}) {
		fmt.Println(t)
	}
	// Output:
}

この関数 Iter は、渡されたスライスの要素を順に列挙するだけのものです。

Go の Wiki によれば、イテレータになれる関数の型は

func (func(V)bool)

func(func(K,V)bool)

のいずれかです。今回は簡単のために1引数版(type Iterator[V any] func(func(V))bool)でいきましょう。

この型が for-range に使える、ということを前提に見てやると... ふむふむ、なるほど、

for v := range ...

v と、

func(func(v V)bool)

v が対応するんだな、ということが察せられます。「ループ変数としてわたってくるもの」は、ループの裏側から見ると「コールバックに渡してやること」と対応しているんですね。 たとえば filepath.Walk なんかと雰囲気が似ています。

では早速動きを見ていきましょう。
go run するのも面倒なので、 testable example を fail させて結果を見てやろうという魂胆です。いざ、 Code Lens から "run test"!

いいですね、思った通りの結果です。

continue, break

Go Wiki によれば、

The “return true” at the end of the body is the implicit “continue” at the end of the loop body. An explicit continue would translate to “return true” as well. A break statement would translate to “return false” instead. Other control structures are more complicated but still possible.

とのこと。つまり、 func(V)boolboolが...

  • true \leftrightarrow continue
  • false \leftrightarrow break

ということのようですね。試してみましょう。

まずは continue から。

package go1_22_rangefn_test

import "fmt"

type Iterator[T any] func(func(T) bool)

func Iter[T any](values []T) Iterator[T] {
	return func(yield func(T) bool) {
		for _, t := range values {
			cont := yield(t)
			fmt.Println("cont:", cont)
			if !cont {
				return
			}
		}
	}
}

func Example() {
	for t := range Iter([]int{1, 2, 3, 4, 5}) {
		fmt.Println(t)
		continue
	}
	// Output:
}

これを実行すると...

1
cont: true
2
cont: true
3
cont: true
4
cont: true
5
cont: true

ふむふむ。

対して break はというと...

package go1_22_rangefn_test

import "fmt"

type Iterator[T any] func(func(T) bool)

func Iter[T any](values []T) Iterator[T] {
	return func(yield func(T) bool) {
		for _, t := range values {
			cont := yield(t)
			fmt.Println("cont:", cont)
			if !cont {
				return
			}
		}
	}
}

func Example() {
	for t := range Iter([]int{1, 2, 3, 4, 5}) {
		fmt.Println(t)
		break
	}
	// Output:
}

こうして、実行しましょう...

--- FAIL: Example (0.00s)
got:
1
cont: false

こうなりました。なるほどね。仕様として示されている通りにみえます。

return や panic は?

for 文の中からは、当然 returnpanic もできますよね。
rangefunc で returnpanic したらどうなるんでしょう?

まずは return

package go1_22_rangefn_test

import "fmt"

type Iterator[T any] func(func(T) bool)

func Iter[T any](values []T) Iterator[T] {
	return func(yield func(T) bool) {
		defer func() {
			rec := recover()
			fmt.Println("defer with recover:", rec)
		}()
		for _, t := range values {
			cont := yield(t)
			fmt.Println("cont:", cont)
			if !cont {
				return
			}
		}
	}
}

func Example() {
	for t := range Iter([]int{1, 2, 3, 4, 5}) {
		fmt.Println(t)
		return  // <--
	}
	fmt.Println("after loop")
	// Output:
}

Iterの側にdeferをくっつけてみました。また、for の後にもログを追加しました。
さあ、実行しましょう!

1
cont: false
defer with recover: <nil>

ほっほう。

  • return したときも、yieldfalse で返るんですね。
  • で、ちゃんと Iter 内の defer も呼び出される、と。
  • その上で、for の先には処理が進まないようになっている。

なるほどねえ。 Iter 側でのクリーンアップ処理のチャンスはきちんと与えつつ、全体としては return らしい挙動になっています。

次は panic させてみましょう。

package go1_22_rangefn_test

import "fmt"

type Iterator[T any] func(func(T) bool)

func Iter[T any](values []T) Iterator[T] {
	return func(yield func(T) bool) {
		defer func() {
			rec := recover()
			fmt.Println("defer with recover:", rec)
		}()
		for _, t := range values {
			cont := yield(t)
			fmt.Println("cont:", cont)
			if !cont {
				return
			}
		}
	}
}

func Example() {
	for t := range Iter([]int{1, 2, 3, 4, 5}) {
		fmt.Println(t)
		panic(42)  // <--
	}
	fmt.Println("after loop")
	// Output:
}

この結果は

1
defer with recover: 42
after loop

こうでした。ふつうに yield 内で panic したような挙動になっています。
Iterrecover したからか、ループの先まで処理が進んでいますね。
なるほどなあ。

戻り値を無視してみようぜ

これまで、yield の戻り値が false だったときだけループを抜けるように書いてきました。
無視してみましょう。

まずは「true が返されてもループを抜けちゃう」パターンから。

package go1_22_rangefn_test

import "fmt"

type Iterator[T any] func(func(T) bool)

func Iter[T any](values []T) Iterator[T] {
	return func(yield func(T) bool) {
		for _, t := range values {
			cont := yield(t)
			fmt.Println("cont:", cont)
			return  // <--
		}
	}
}

func Example() {
	for t := range Iter([]int{1, 2, 3, 4, 5}) {
		fmt.Println(t)
	}
	fmt.Println("after loop")
	// Output:
}

こうですね。Iter は問答無用で return します。すると...

1
cont: true
after loop

何事もなく、普通に抜けてきました。

では、 「false が返されたのにループをやめない」パターンはどうでしょう?

まずは break から。

package go1_22_rangefn_test

import "fmt"

type Iterator[T any] func(func(T) bool)

func Iter[T any](values []T) Iterator[T] {
	return func(yield func(T) bool) {
		for _, t := range values {
			cont := yield(t)
			fmt.Println("cont:", cont)
			// return なし!
		}
	}
}

func Example() {
	for t := range Iter([]int{1, 2, 3, 4, 5}) {
		fmt.Println(t)
		break  // <--
	}
	fmt.Println("after loop")
	// Output:
}

これは...

panic: runtime error: range function continued iteration after exit [recovered]
	panic: runtime error: range function continued iteration after exit

goroutine 1 [running]:
testing.(*InternalExample).processRunResult(0xc000125c08, {0xc000012260, 0xe}, 0xc000125908?, 0x0, {0xc6a3e40, 0xc788850})

...(略)

あっ、 panic しました!

return についても試しましたが、こちらもやはり panic しました。
無視するのは許さん、ということなんですね。

では、いったいどれだけ無視したらダメなんでしょう? ちょっとはアリなんでしょうか。

package go1_22_rangefn_test

import "fmt"

type Iterator[T any] func(func(T) bool)

func Iter[T any](values []T) Iterator[T] {
	return func(yield func(T) bool) {
		for _, t := range values {
			fmt.Println("before: ", t)
			cont := yield(t)
			fmt.Println("after:", t, " / cont:", cont)
		}
	}
}

func Example() {
	defer func() {
		recover()
	}()
	for t := range Iter([]int{1, 2, 3, 4, 5}) {
		fmt.Println(t)
		break
	}
	fmt.Println("after loop")
	// Output:
}

こんなんやってみましょう。ログはどうなるでしょう?

before:  1
1
after: 1  / cont: false
before:  2

こうなりました。次の yield 呼び出しの瞬間に panic になるみたいですね。

まとめ

rangefunc の挙動を調べてみました。

  • 「ループブロックへの制御戻し」が「コールバック呼び出し」として表現されている
  • コールバックの戻り値と、ループを続行するかどうかが対応している
  • ループ終了を無視すると panic

...ということがわかりました。

あー楽しかった!

Discussion