🆕

Go 1.23 range over funcことはじめ: メソッドチェインで書けるフィルタを作ってみた

2024/08/01に公開

はじめに - range over funcとは

Go 1.22からPreviewとしてリリースされ、Go 1.23で正式リリース予定となっている機能でいわゆるイテレータを実現するためのものです

https://github.com/golang/go/issues/61405

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() boolfunc(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 funcrangeキーワードと組み合わせることで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
        }
    }
})

yieldrange 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

https://go.dev/play/p/kLRhmQZ37_K?v=gotip

作ったもの - xtract

https://github.com/miyamo2/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

といった形で使い分けてください

ByValueByKeyAndValueLimitはRuss Coxがプロポーザルを出しているx/exp/xiterFilter[V any]Filter2[K,V any]Limit[V any]とおおむね同じ実装ですが、miyamo2/xtractはメソッドチェインで書けるのでJavaのStream API等と近い書き味になるかと思います

また、miyamo2/xtractでは全ての中間処理は終端処理(現状Valuesのみ)が呼ばれたタイミングで評価されるため、そういった点でもStream APIと近いといえます
例としてサンプルコードのevenevenAndTwoDigitsでは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も作ってみました

https://zenn.dev/comsize_press/articles/30985199012029

Discussion