go の iterator functionで遊ぶ
range over function周りとかで遊ぶ。
そもそもiterator functionで良いんだろうか?呼び方は。
https://go.dev/ref/spec#For_range
A "for" statement with a "range" clause iterates through all entries of an array, slice, string or map, values received on a channel, integer values from zero to an upper limit [Go 1.22], or values passed to an iterator function's yield function [Go 1.23].
iter package
- SeqX, push iterator, 内部Iterator
- PullX, pull iterator, 外部iterator
例えば、複数のstreamを少しずつ読んでいくみたいなやつがpull iteratorを使いたくなりそう
release note
release noteに挙げられていたblog
spec はこの辺?
実装を覗く
- slices, maps, ...
とかあったきがする。とりあえずslices.All()とかをみるか。
// All returns an iterator over index-value pairs in the slice
// in the usual order.
func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] {
return func(yield func(int, E) bool) {
for i, v := range s {
if !yield(i, v) {
return
}
}
}
}
とりあえず関数を返せば良い。引数として取ることでyieldとかを予約語にしないのは面白い。
自分で書いてみる
とりあえずintegerあたりから試してみるか。
数値をfor loopで回すやつ。
自分で作るやつ iter.Seq[int]
を作るためには関数を返す。
pythonでいうrange()みたいなものはないのかな。とりあえず自作してみる。
あと、slices.Collect()
でslicesに変換できるみたい。
間にLogger的なものを差し込んだ時にしっかりyieldの値を見ないと止まらなくなる
for i := range Logger(Gen(5)) { if i == 3 { break; } }
Pullも使ってみるか。
テキトーに複数のSeqをマージするやつ。これだと順序も欲しそう。
このコードは全部読んでいるけれど↓のような感じの方が使い道があるかもしれない。
例えば、複数のstreamを少しずつ読んでいくみたいなやつがpull iteratorを使いたくなりそう
yieldで自作した型越しに順序も返すようにしてみた。返すsliceの長さが変わるのはだるいかもしれない?ただ使い回す形にするとポインタが現れてしまいそう。。
いわゆる Traversable みたいなあれはできるんだろうか?
💭 tree 的なものを辿る系のものは食傷気味
ここからは二日目
テキトーにいわゆるMap()を作ってみる。そういえば共通のunderlying typeを持つものにも対応できた方が良い?(そうでもない気もする)
iter.Seq2[int, T]
みたいなものが作りたければ slices.All()
が使えたけれどそれとは型が合わない。
たとえばAllは以下のようなシグネチャになっている。 func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]
underlying typeに []E
を持っているようなものも対象になる。
試しに比較用のコードを書いてみたけれど、あんまり嬉しくはない気がした(そしてこのコードはiterator functionの確認用のコードというよりはgenericsの確認用のコードだ)
2分木のiterator functionの例自体は簡単に何も見ずに書けるけれどもう少し綺麗だった気がする。
(記憶が確かなら論理演算で繋げてたような記憶)
ちなみにテキストで図示するのにChatGPTが便利だった
以下の値を2分木とみなして良い感じにテキストで図示してください
[[1], 10, [[20,[50]], 100]]]
このリストは2分木の構造を持っています。リスト内の各要素がツリーのノードを表しており、左側>のサブリストが左の子ノード、右側のサブリストが右の子ノードを表しています。
リスト [[1], 10, [[20,[50]], 100]] を木として表すと、次のような形になります。10 / \ 1 100 / 20 \ 50
2分木のiterator functionの例自体は簡単に何も見ずに書けるけれどもう少し綺麗だった気がする。
(記憶が確かなら論理演算で繋げてたような記憶)
あー、blogやwikiじゃなくてissueか
func (t *Tree[V]) All() iter.Seq[V] {
return func(yield func(V) bool) { t.all(yield) }
}
func (t *Tree[V]) all(yield func(V) bool) bool {
return t == nil ||
t.left.all(yield) && yield(t.value) && t.right.all(yield)
}
書き直してみた
ここからは三日目
goのrange over functionってクロージャへの変換だから普通にbreakして再開した場合にはしっかり最初からだけれど
引数の状態を持ち越したりして変数をキャプチャしてクロージャを作っちゃうと途中からになってしまうのだなー。
こういう感じ
func gen(n int) iter.Seq[int] {
i := 0
return func(yield func(int) bool) {
for ; i < n; i++ {
if !yield(i) {
return
}
}
}
}
こういうコードをphantom type的な感じで区別できないんだろうか?(厳しそう)
あと io.ReadCloser的なもののopen/closeのハンドリングとio.EOFとかのハンドリングをしてもらえると便利なんだろうか?
bufio.Scannerを使うとio.EOFを直接触ることはない気がする。でもエラーハンドリングを最後にやりたくはなるのか ( https://zenn.dev/link/comments/02b9956ba98fcf )。
it, errFunc := feeder(r) // iter.Seq[string], func() error
⚠️ ここで以下のような呼ばない関数をnilと比較するのは危険かも。。
if err := errFunc; err != nil { // errFunc() !!
panic(err)
}
これで怒られが発生して欲しい (ちょっとerrorハンドリングを真面目にした)
あと、WithIndexみたいなものは欲しいかも?
func WithIndex[T any](iter.Seq[T]) iter.Seq2[int, T]
WithIndex()が欲しいと言っても結局のところ手軽さだけで単に自前で変数を管理すれば良いだけという気もする。
しかしスコープを可能な限り狭めようとするとブロックが1段増える。他に書き方は無いんだろうか?
{
i := 0
for line := range it {
fmt.Println(i, line)
i++
}
}
外部のパッケージ
今なら iter パッケージの imported byを見れば良いのでは?
エラーハンドリング
yieldの最中でエラーが出るやつをどうするか?
-
iter.Seq2[_, error]
を返す -
iter.Seq[_], func() error
を返す
iter.Seq2[_, error] を返す
google apiとかだとseq2の方かも
func (it *CollectionItemIterator) All() iter.Seq2[*visionaipb.CollectionItem, error]
iter.Seq[_], func() error を返す
これは議論か何かの最中で見かけたような気がする。どこだっけ?
func (Map[K, V]) Items() (iter.Seq2[K, V], func() error)
⚠️func() error
を呼び出し忘れてnilと比較してしまうことがあったりする
future works
- 何かログを出力したい時に context.Context を取りたくなることもあるかもしれない?
- Either monadとか Validation monad みたいな感じのこともある?
issues
たとえば 1.23, 1.24 のやつを見る
- is:issue label:Proposal-Accepted label:compiler/runtime milestone:go1.23
- is:issue label:Proposal-Accepted label:compiler/runtime milestone:go1.24
titleにiterが現れたらそれなんだろうか?
何か思いついたことをコメントに残す
意外と playground だけで遊べなくもない
TODOが残ったままになっているのはだるいかもしれない。ところでこのscrapはいつcloseするんだろう?
issueを集める用のlinkになったので意外とたまに開くページになるかもしれない
他の人が書いた記事とかを全然みてない
必ず欲しくなる関数はなんだろう?
- Gen
func(int) Seq[int]
みたいなやつ - Chunk
func(Seq[int), n) Seq[[]int]
みたいなやつ
chunkはslicesに実装がある。 https://pkg.go.dev/slices#Chunk
標準ライブラリのどこそこに使われるようになるみたいな情報にはあまり興味がなさそう
遊ぶというよりは調べてる気がするし、もうタイトルからかけ離れたscrapになったかもしれない
channelと組み合わせた時の挙動とかがわかっていないかもしれない?
- 例えばtqdmみたいなことをしたい時に使えるか?
- cursor的なUIのAPIを辿るときに良い感じに使えるか?
💭そういえば、思考に制約をかけて色々な書き方ができないのが便利みたいな感じにはならなくなったかもしれない。高階関数を作れてしまったら色々やりたくなってきそう(どこかで書いたcontext対応とかエラー対応とかが含まれるとさらにカオスになる)。
この辺りはせいぜい小さなサブセットだけが必要になるのかそれとも現在存在する関数群の亜種がセットで必要になっていくのかわからない(async polution的な話?)。少なくともエラーが発生しうるようなものはgoには例外がないので別セットみたいになりそう。
計算にだけ使うという割り切りもありな気がするが、そうするとvalidation的なものには使えなくなるのだなーという感じ。
pythonだとgeneratorとかが対応しそう(sendする機能を除く)。この辺が野坊主に追加されまくっていくと厳しそう
chunkedの他に windowed とか combinations とかもたまに欲しくなるかもしれない
そういえば、channelと違って何度もzero値とfalseを返してくれるのだった(pull型文脈)。なので無限ループ的なものを消費しておいてbreakみたいなことは容易なのだった。
少し前に考え事をしていたようだ
直接使う例は面白い。つまるところrangeのときにコンパイラが頑張ってくれる
func PrintAllElements[E comparable](s *Set[E]) {
s.All()(func(v E) bool {
fmt.Println(v)
return true
})
}
なるほど range over function だ
以下も手慰みにやっても良いかも?
- treeのwalk
- error handling
コールスタックのことは考えてなかったな
(おそらくwikiのインライン展開の記事にあったようなあれでも変わる?(コスト換算の結果にもよる))
真面目に記事にするならエラーハンドリングとかのところで既存の方法の実装例を載せるとかありそう(e.g. stripe-go, elasticsearch など)
treeのwalkを書いてみた。all()とAll()のメソッドを定義するコードは綺麗だな。
もうすこしまじめに実装を読む
そういえば、クロージャに変換されるのでiterator functionに渡される引数を初期化するかそのまま使うかで微妙にrangeで2回消費した後の挙動が変わる?
bufio.Scannerとかを使うとエラーハンドリングの2番目の方法が使いたくなるんだな。そして関数型はnilと比較できちゃうから辛い。
slices.All()ではなくslices.Values()ならsliceをiter.Seqにできるのでは?