Go 1.23 range over funcことはじめ: メソッドチェインで書けるフィルタを作ってみた
はじめに - range over funcとは
Go 1.22からPreviewとしてリリースされ、Go 1.23で正式リリース予定となっている機能でいわゆるイテレータを実現するためのものです
database/sqlで実装されているようなHasNextで存在確認、Nextでカーソルを進めるnext-based|pull-basedインターフェイスでなく、for range構文と統合されたyield-based|push-basedインターフェイスが採用されています
// next-based(pull-based)
for pullIter.HasNext() {
pullIter.Next()
pullIter.Something()
}
// yield-based(push-based)
for k, v := range pushIter {
fmt.Printf("%v: %v\n")
}
yieldとrange over func
yieldはPython等と違い予約語ではなく、引数を0~2つ取ってboolを返す関数となっています
yieldはループが継続されていれば(中断されていなければ)true、継続されていなければ(中断されていれば)falseを返します
(あくまでデバッグ実行を踏まえての推論ですが...)range over funcを用いたfor rangeではforブロック内の処理が:= rangeの左辺に応じてfunc() bool、func(T) boolもしくはfunc(T, U) boolのいずれかにラップされ、yieldとして扱われます
for i, v := range someIter {
fmt.Sprintf("%d: %s", i, v)
}
// ※イメージです
func(i int, v string) (called bool) {
// 不思議な力によってcalledが解決される
fmt.Sprintf("%d: %s", i, v)
return
}
range over funcはrangeキーワードと組み合わせることでyieldもとい、forブロック内の処理を受け取って実行することができる関数です
yieldがfalseを返した際、つまりfor rangeが中断された場合には処理を終了させる必要があります
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
sq := iter.Seq[int](func(yield func(int) bool) {
for _, v := range s {
if v%2 == 0 && !yield(v) {
return
}
}
})
range over func自体はただの関数なので勿論for range以外からrange over funcを実行することも可能です
その場合はbreakをしないfor rangeと同等の挙動となる認識です
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
sq := iter.Seq[int](func(yield func(int) bool) {
for _, v := range s {
if v%2 == 0 && !yield(v) {
return
}
}
})
//for v := range sq {
// fmt.Printf("%d\n", v)
//}
sq(func(v int) bool {
fmt.Printf("%d\n", v)
}
// Output: 2
//4
//6
//8
//10
作ったもの - xtract
package main
import (
"fmt"
"github.com/miyamo2/xtract"
"strings"
)
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 100, 101}
xt := xtract.FromSlice(s)
even := xt.ByValue(func(i int) bool { return i%2 == 0 })
odd := xt.ByValue(func(i int) bool { return i%2 != 0 })
fmt.Println("---even---")
for v := range even.Values() {
fmt.Println(v)
}
fmt.Println("---odd---")
for v := range odd.Values() {
fmt.Println(v)
}
evenAndTwoDigits := even.ByValue(func(i int) bool { return i > 9 && i < 100 })
fmt.Println("---even and two digits---")
for v := range evenAndTwoDigits.Values() {
fmt.Println(v)
}
oddAndTwoDigits := odd.ByValue(func(i int) bool { return i > 9 && i < 100 })
fmt.Println("---odd and two digits---")
for v := range oddAndTwoDigits.Values() {
fmt.Println(v)
}
// Output: ---even---
//0
//2
//4
//6
//8
//10
//100
//---odd---
//1
//3
//5
//7
//9
//11
//101
//---even and two digits---
//10
//---odd and two digits---
//11
}
上記サンプルコードではByValueしか使用していませんが、
- 値でフィルタをする場合は
ByValue - キーでフィルタをする場合は
ByKey - 値とキーでフィルタをする場合は
ByKeyAndValue - 先頭N件にフィルタする場合は
Limit - 先頭N件以降にフィルタする場合は
Offset
といった形で使い分けてください
ByValue、ByKeyAndValue、LimitはRuss Coxがプロポーザルを出しているx/exp/xiterのFilter[V any]、Filter2[K,V any]、Limit[V any]とおおむね同じ実装ですが、miyamo2/xtractはメソッドチェインで書けるのでJavaのStream API等と近い書き味になるかと思います
また、miyamo2/xtractでは全ての中間処理は終端処理(現状Valuesのみ)が呼ばれたタイミングで評価されるため、そういった点でもStream APIと近いといえます
例としてサンプルコードのevenとevenAndTwoDigitsではFromSlice(s)からByValue(func(i int) bool { return i%2 == 0 })までの中間処理がそれぞれの終端処理で実行される挙動となっています
現状コレクションを別の型に加工するいわゆるmapメソッドが実装できていないのですが、あくまで型安全にこだわりたかったことや、Goが今後可変長型パラメータをサポートすることへの期待も込めて今は無理に実装しないことにしました
余談
v0.1.0時点では元ネタのslice, mapをそれぞれ保持していた都合でSliceExtractor[V any]、MapExtractor[K comparable, V any]と実装が分かれているのですが、iter.Seq2[K, V any]を保持する形に変更したため、無用の長物となってしまいました
なんとかして実装が分かれていることを活かしたいので良きアイデアのある方は是非PRかIssueを送ってください...!(勿論それ以外の内容も大歓迎です)
range over funcを触ってみた所管
ライブラリ開発の場面ではユーザーに大きな利便性を提供できると感じました
個人的にdatabase/sqlが一番対応してほしい標準パッケージです
追伸: miyamo2/filtgen
構造体の定義に応じてフィルタを自動生成するCLIも作ってみました
Discussion