Closed62

go の iterator functionで遊ぶ

podhmopodhmo

range over function周りとかで遊ぶ。

podhmopodhmo

そもそも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].

podhmopodhmo

iter package

https://pkg.go.dev/iter

  • SeqX, push iterator, 内部Iterator
  • PullX, pull iterator, 外部iterator

例えば、複数のstreamを少しずつ読んでいくみたいなやつがpull iteratorを使いたくなりそう

podhmopodhmo

release note

https://go.dev/doc/go1.23

release noteに挙げられていたblog

https://go.dev/blog/range-functions

podhmopodhmo

とりあえず関数を返せば良い。引数として取ることでyieldとかを予約語にしないのは面白い。

podhmopodhmo

自分で書いてみる

podhmopodhmo

Pullも使ってみるか。

テキトーに複数のSeqをマージするやつ。これだと順序も欲しそう。

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

このコードは全部読んでいるけれど↓のような感じの方が使い道があるかもしれない。

例えば、複数のstreamを少しずつ読んでいくみたいなやつがpull iteratorを使いたくなりそう

podhmopodhmo

ここからは二日目

テキトーにいわゆるMap()を作ってみる。そういえば共通のunderlying typeを持つものにも対応できた方が良い?(そうでもない気もする)

iter.Seq2[int, T] みたいなものが作りたければ slices.All() が使えたけれどそれとは型が合わない。

https://go.dev/play/p/9UWPQdsl8KQ

たとえばAllは以下のようなシグネチャになっている。 func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] underlying typeに []E を持っているようなものも対象になる。

試しに比較用のコードを書いてみたけれど、あんまり嬉しくはない気がした(そしてこのコードはiterator functionの確認用のコードというよりはgenericsの確認用のコードだ)

https://go.dev/play/p/V3AjIFEEH8K

podhmopodhmo

2分木のiterator functionの例自体は簡単に何も見ずに書けるけれどもう少し綺麗だった気がする。
(記憶が確かなら論理演算で繋げてたような記憶)

https://go.dev/play/p/ZJ2EE_Tg8Zk

ちなみにテキストで図示するのにChatGPTが便利だった

以下の値を2分木とみなして良い感じにテキストで図示してください

 [[1], 10, [[20,[50]], 100]]]

このリストは2分木の構造を持っています。リスト内の各要素がツリーのノードを表しており、左側>のサブリストが左の子ノード、右側のサブリストが右の子ノードを表しています。
リスト [[1], 10, [[20,[50]], 100]] を木として表すと、次のような形になります。

      10
     /  \
    1   100
        /
       20
        \
        50
podhmopodhmo

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)
}

https://github.com/golang/go/issues/61405#issuecomment-1638896606

書き直してみた

https://go.dev/play/p/t-kDHpRUocn

podhmopodhmo

ここからは三日目

goのrange over functionってクロージャへの変換だから普通にbreakして再開した場合にはしっかり最初からだけれど

https://go.dev/play/p/W521u0eyXr-

引数の状態を持ち越したりして変数をキャプチャしてクロージャを作っちゃうと途中からになってしまうのだなー。

https://go.dev/play/p/noFWsI3pKeG

こういう感じ

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的な感じで区別できないんだろうか?(厳しそう)

podhmopodhmo

あと io.ReadCloser的なもののopen/closeのハンドリングとio.EOFとかのハンドリングをしてもらえると便利なんだろうか?

bufio.Scannerを使うとio.EOFを直接触ることはない気がする。でもエラーハンドリングを最後にやりたくはなるのか ( https://zenn.dev/link/comments/02b9956ba98fcf )。

https://go.dev/play/p/7fUhOZ2rbk1

it, errFunc := feeder(r) // iter.Seq[string], func() error

⚠️ ここで以下のような呼ばない関数をnilと比較するのは危険かも。。

if err := errFunc; err != nil { // errFunc() !!
    panic(err)
}

これで怒られが発生して欲しい (ちょっとerrorハンドリングを真面目にした)

https://go.dev/play/p/EStppOpf-jZ

podhmopodhmo

WithIndex()が欲しいと言っても結局のところ手軽さだけで単に自前で変数を管理すれば良いだけという気もする。

https://go.dev/play/p/H4mMSA6zbWv

しかしスコープを可能な限り狭めようとするとブロックが1段増える。他に書き方は無いんだろうか?

{
		i := 0
		for line := range it {
			fmt.Println(i, line)
			i++
		}
}
podhmopodhmo

エラーハンドリング

yieldの最中でエラーが出るやつをどうするか?

  • iter.Seq2[_, error] を返す
  • iter.Seq[_], func() error を返す
podhmopodhmo

future works

  • 何かログを出力したい時に context.Context を取りたくなることもあるかもしれない?
  • Either monadとか Validation monad みたいな感じのこともある?
podhmopodhmo

記事

あとで

podhmopodhmo
podhmopodhmo

何か思いついたことをコメントに残す

podhmopodhmo

TODOが残ったままになっているのはだるいかもしれない。ところでこのscrapはいつcloseするんだろう?

podhmopodhmo

標準ライブラリのどこそこに使われるようになるみたいな情報にはあまり興味がなさそう

podhmopodhmo

遊ぶというよりは調べてる気がするし、もうタイトルからかけ離れたscrapになったかもしれない

podhmopodhmo

channelと組み合わせた時の挙動とかがわかっていないかもしれない?

  • 例えばtqdmみたいなことをしたい時に使えるか?
  • cursor的なUIのAPIを辿るときに良い感じに使えるか?
podhmopodhmo

💭そういえば、思考に制約をかけて色々な書き方ができないのが便利みたいな感じにはならなくなったかもしれない。高階関数を作れてしまったら色々やりたくなってきそう(どこかで書いたcontext対応とかエラー対応とかが含まれるとさらにカオスになる)。

この辺りはせいぜい小さなサブセットだけが必要になるのかそれとも現在存在する関数群の亜種がセットで必要になっていくのかわからない(async polution的な話?)。少なくともエラーが発生しうるようなものはgoには例外がないので別セットみたいになりそう。

計算にだけ使うという割り切りもありな気がするが、そうするとvalidation的なものには使えなくなるのだなーという感じ。

podhmopodhmo

そういえば、channelと違って何度もzero値とfalseを返してくれるのだった(pull型文脈)。なので無限ループ的なものを消費しておいてbreakみたいなことは容易なのだった。

podhmopodhmo

直接使う例は面白い。つまるところrangeのときにコンパイラが頑張ってくれる

func PrintAllElements[E comparable](s *Set[E]) {
    s.All()(func(v E) bool {
        fmt.Println(v)
        return true
    })
}

なるほど range over function だ

podhmopodhmo

以下も手慰みにやっても良いかも?

  • treeのwalk
  • error handling
podhmopodhmo

コールスタックのことは考えてなかったな

(おそらくwikiのインライン展開の記事にあったようなあれでも変わる?(コスト換算の結果にもよる))

podhmopodhmo

真面目に記事にするならエラーハンドリングとかのところで既存の方法の実装例を載せるとかありそう(e.g. stripe-go, elasticsearch など)

podhmopodhmo

treeのwalkを書いてみた。all()とAll()のメソッドを定義するコードは綺麗だな。

podhmopodhmo

そういえば、クロージャに変換されるのでiterator functionに渡される引数を初期化するかそのまま使うかで微妙にrangeで2回消費した後の挙動が変わる?

podhmopodhmo

bufio.Scannerとかを使うとエラーハンドリングの2番目の方法が使いたくなるんだな。そして関数型はnilと比較できちゃうから辛い。

podhmopodhmo

slices.All()ではなくslices.Values()ならsliceをiter.Seqにできるのでは?

このスクラップは1ヶ月前にクローズされました