【Go】Go1.22で試験導入されたrangefuncを理解したい！

# はじめに
こんにちは、H×Hのセンリツ大好きエンジニアです。（同担OKです😉）
今回は、Go Conference 2024でも取り上げられていた`rangefunc`という新たな機能について自分なりにまとめたので、そちらをご紹介します！

# rangefuncとは？
公式リファレンス参照：https://go.dev/wiki/RangefuncExperiment
> For a future Go release, the Go team is considering adding range-over function iterators. Go 1.22 contains a preliminary implementation of the change, enabled by setting GOEXPERIMENT=rangefunc when building your program. We invite anyone who wants to help us understand the effects of the change to try using GOEXPERIMENT=rangefunc and let us know about any problems or successes encountered.

将来的なイテレータの追加に伴って試験的に追加された機能です。
`range`に**関数が選択できる**ようになるという便利なものだそう。。。ほえ〜😮

（試験的な導入なので、利用する場合は`GOEXPERIMENT=rangefunc`を実行時に含める必要があるとのこと）

[Go 1.22で追加予定のrange over intと、GOEXPERIMENT入り予定のrange over funcを触ってみる](https://tech.every.tv/entry/2023/12/09/1)
上記のeveryさんの記事によるとどうやら、現在のGoにはイテレータの標準がないために同じようなコードで実装出来ていないようです。

そのため、`range`を拡張する形でGoのイテレータの標準化を行うという背景だそうですね〜。

確かに、Goは同じようなコードになる事による保守性が魅力の一つなので、書き方がバラバラになっちゃうと分かりづらくなりますもんね。

# rangefuncを触ってみる
公式リファレンスにあるコードを少しいじってみました。
``` main.go
package main

import "fmt"

type Seq[V any] func(func(V) bool)

func Range[V any](s []V) Seq[V] {
	return func(yield func(V) bool) {
		for _, v := range s {
			if !yield(v) {
				return
			}
		}
	}
}

func main() {
	s := []string{"hello", "world", "!"}
	for v := range Range(s) {
		fmt.Println(v)
	}
}
```
このコードを実行するには、`GOEXPERIMENT=rangefunc`を設定します。
結果は以下のようになります。
``` zsh
$ GOEXPERIMENT=rangefunc go run main.go
hello
world
!
```

これ見ても何のこっちゃ🤔だと思うので、一つづつ解説していきます！
``` main.go
type Seq[V any] func(func(V) bool)
```
`Seq[V]`というジェネリックな型を定義しています。
引数としては関数`func(V) bool`を取る関数を表しています。

`func(func(V) bool)`は、`rangefunc`でイテレータになれる関数の型の一つになっています。
イテレータになれる関数の型は以下の通り
- `func(func(V) bool)`
- `func(func() bool)`
- `func(func(K, V) bool)`

``` main.go
func Range[V any](s []V) Seq[V] {
	return func(yield func(V) bool) {
		for _, v := range s {
			if !yield(v) {
				return
			}
		}
	}
}
```

`Range[V]`はジェネリックな関数で、渡されたスライスの要素を順番に参照し、関数として返却する関数となっています。
引数のスライスの各要素に対して`yield`関数を適用し、`yield`が`false`を返すとループを終了するようです。🥴

``` main.go
func main() {
	s := []string{"hello", "world", "!"}
	for v := range Range(s) {
		fmt.Println(v)
	}
}
```

`main`関数では、文字列のスライスを作成し、`Range`関数を呼び出しています。
`Range(s)`が返す関数に対して、各要素を処理するための関数を渡し、`fmt.Println(v)`で各要素を表示しています。

。。。なるほど、ざっくり理解しました。😇
`Range`にある`yield`の役割は、反復を続けるかどうかを決めるものなので、`break`文によってループを抜け出す場合などに`false`を返却するとのこと。

なので、途中でbreakする処理を追加すると。。。
``` main.go
package main

import "fmt"

type Seq[V any] func(func(V) bool)

func Range[V any](s []V) Seq[V] {
	return func(yield func(V) bool) {
		for _, v := range s {
			if !yield(v) {
				return
			}
		}
	}
}

func main() {
	s := []string{"hello", "world", "!"}
	for v := range Range(s) {
		fmt.Println(v)
		if v == "world" {
			break
		}
	}
}
```

実行してみます。
``` zsh
$ GOEXPERIMENT=rangefunc go run main.go
hello
world
```

`world`が`Range`に渡された時に`break`が走ることによって、`yield`が`false`になるため`return`が返却されて処理が終わります。

# まとめ
今回は個人的に気になっていた`rangefunc`をざっくり理解するテーマでお送りしました。
実際、現場で開発する際にどのケースで使うかは把握できていませんが、以下の記事によると大きなスライスをチャンクに分けて処理する際に効果的っぽいですね。知りませんけど😇

[Go1.22で登場するかも！？開発中の新機能イテレータを使ってみる！](https://zenn.dev/team_soda/articles/0b57bba3a3665f)

最後までご覧いただきありがとうございました。
センリツでした🤓