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) {
// 不思議な力(高階関数にバインドされたchannelかも?)
// によって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
}
}
})
yield
、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
が一番対応してほしい標準パッケージなのですが、仮に対応したとしてSQLドライバがすぐに追従するとも思えないので、ライブラリの1ユーザーとして恩恵を受けるのは当分先になりそうな気はします
追伸: miyamo2/filtgen
構造体の定義に応じてフィルタを自動生成するCLIも作ってみました
Discussion