タラバガニー設計局stalins.clubNOTE/notes/iter-seq-range-over-func

iter.Seq と range-over-func

iter.Seq[E]func(yield func(E) bool) という関数型で、Go 1.23 の range-over-func により for v := range s の形でそのまま回せる。イテレータを表す型そのものは 1.23 で標準に入った。

しかし iter.Seq[E] を繋いで加工する Map / Filter のようなアダプタは、go1.27rc3 時点でも標準ライブラリに無い。x/exp/xiter (#61898) がその一式を提案していたが 2025-05-21 に declined as retracted になっている (xiter は取り下げ済み)。標準に無い以上、自分でパッケージレベル関数として書くしかなく、Apply(Apply(words.Filter(isLong), toUpper), label) のように内側から外側へ読む形 (inside-out) になる。イテレータの利点である「繋いでも中間スライスができない」という性質も、この読みにくさに埋もれてしまう。

ここに ジェネリクスメソッド (Go 1.27) が効く。Stream[E]iter.Seq[E] の defined type として定義し、Map をそのメソッドにすると、Map が返す型 F はレシーバのどこにも書かれていないため新しい型パラメータが要る。これが Go 1.26 まで書けなかった部分そのものである (メソッドはレシーバを持つ関数 の「レシーバ由来では決まらない型」の一例)。

type Stream[E any] iter.Seq[E]
func (s Stream[E]) Map[F any](f func(E) F) Stream[F] {
	return func(yield func(F) bool) {
		for v := range s {
			if !yield(f(v)) {
				return
			}
		}
	}
}

使う側はメソッドチェーンで書ける。Java の Stream や Rust の Iterator に相当するものが自分で書けるようになる。

got := Stream[string](slices.Values(words)).
	Filter(func(s string) bool { return len(s) > 2 }).
	Map(strings.ToUpper).
	Map(func(s string) int { return len(s) }).
	Collect()

注意すべき点が2つある。ひとつは、新しい型パラメータが要るのは Map だけということ。FilterCollect は戻り値の型がレシーバの E だけで決まるので、Go 1.26 でも普通にメソッドにできた。もうひとつは、Map は新しい Stream[F] を返すだけで何も実行しないということ。実際に1回流れるのは Collect() が range した瞬間で、それまでは遅延したままになる。

実測では Stream[E] の underlying が func 型なので、for v := range siter.Seq[E] への明示的な変換なしにそのまま通る (go1.27rc3 で確認済み)。また Map(strings.ToUpper) のように関数をそのまま渡せるのは型推論が効いているためで、f func(E) FF は引数の関数から決まる。

出典

  • Go Release Party 1.27 の発表資料リポジトリ slide/deck.md (stream / stream-use スライド)
  • 同リポジトリ memo.md §7 (2026-08-16 追記)
  • #61898

▸ ノート一覧に戻る