Go1.22 の実験的機能 rangefunc で遊んでみた
Go 1.22 で実験的機能として rangefunc が試せるようになっています。
今日はちょっとコイツで遊んでみようと思います。
まずは使ってみよう
環境変数 GOEXPERIMENT=rangefunc
をセットすると使えるようになります。
というわけで、 VSCode にチョチョイと設定を入れてやって試しましょう。
{
"go.toolsEnvVars": {
"GOEXPERIMENT": "rangefunc"
}
}
動かしてみたいだけですからね、すごい単純なやつでいいです。
たとえばこんなの
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)bool
のbool
が...
-
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 文の中からは、当然 return
や panic
もできますよね。
rangefunc で return
や panic
したらどうなるんでしょう?
まずは 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
したときも、yield
がfalse
で返るんですね。 - で、ちゃんと
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
したような挙動になっています。
Iter
で recover
したからか、ループの先まで処理が進んでいますね。
なるほどなあ。
戻り値を無視してみようぜ
これまで、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