GoのイテレータにRubyっぽいコレクション操作を提供するパッケージを作った

GitHub - aereal/iter: iter provides utility functions about standard iter.

使い方

pkg.go.devを見てもらえるとよい。

Chunkを取り上げると、第一引数を n 個ごとの要素に分割したイテレータを返す。

たとえば一度に最大500個までの引数を受け取るAPIへリクエストを送る処理を実装する場合、送りたい引数リストを500件ずつに分割して送りたい。 そういう時に Chunk を使うとよい。

for chunk := range seq.Chunk(args, 500) {
  _ = sendRequest(ctx, chunk)
}

他に、2つの iter.Seq を引数にとり同じ添字に位置する要素をペアにした iter.Seq2 を返す Zip などもある。

モチベーション

Rubyなどにあるような高級なコレクション操作が言語を問わず使えると嬉しい。「ある条件を満たす限り先頭から要素をとりつづけて~」と説明するより「takeWhileしたい」と言って通じるほうが断然話が早いのは間違いない。

高級なコレクション操作をGo向けに提供する試みに対してこれまで個人的に否定的な態度をとってきたが、2つの変化が追い風となり便利さが勝ったと判断したので作ることにした。

理由のひとつがジェネリクスの導入。

ジェネリクス導入以前のGoは型システムが貧しく、コレクション操作を一般化しようとすると、コード生成を用いて利用者が使う具象型ごとに適応させるかさもなくば any (interface{}) を使ってキャストに頼るかしかなかった。

素朴なコレクション操作のためにひたすらにコード生成を強いるのは利用者の負担が大きいし、そのようなライブラリ・ツールを実装することを考えると割に合わない気がした。

キャストする場合、当然コレクションと見なせない型であれば実行を停止せざるをえないが、panicしてシグネチャからエラーを取り除くにせよ、キャスト失敗をエラーとして伝えるシグネチャにするにせよ、利便性や安全性などの観点でそれぞれ懸念がある。

ジェネリクスはこれらの問題を(ほぼ)解決してくれる。

もうひとつの理由がイテレータの整備、より具体的にはrange-over-funcの登場である。

対象とするコレクションがどんな性質か・コレクションを加工としてどんなデータを得たいかによって効率的なプログラムの書き方が変わってくる。

Goのコレクション操作ライブラリの利用者は、for文を使って何度も書いてきた定型的な操作を任せたいのであって、コレクションそのもののサイズやキャパシティ管理だとか排他制御だとかまでを手放したいわけではない。

range-over-funcの導入で拡充されたイテレータは、これまで言語仕様で特定の型だけを特別扱いして規程されていた反復処理の実装を利用者が制御できるよう拡張しつつ、イテレータプロトコルに則って先に挙げたデメリットを解消ないし抑えて一般化しやすくするもので、これが最後の後押しとなった。

イテレータプロトコルに則れば途中で反復を止めることも可能だから、無限リストやストリームのようなコレクションも適切に扱える。

実際、 Zip はpull型のイテレータを使っているので、渡したイテレータが勝手に終端しない無限リストのような振る舞いをしても要素を反復できる。

むすび

Go自身の進化によりコレクション操作のライブラリが提供・利用しやすくなったので恩恵に最大限与るために作ったよ、というご紹介だった。

なんでも入れるつもりはなくて、たとえばmapのような操作は十分に単純なので入れるつもりはない。

また、イテレータからスライスであるとかマップであるとか具象へ変換するような処理やいわゆる畳み込みに類されるものの導入も消極的。

端的に言うと range の右側に書く組み合わせ可能なグッズだけ集めることに価値を見出しているかんじ。

Goの見た目をRubyとかScalaっぽくするジョークグッズを作るつもりはなく、あくまで普段からよく使うが初見でテストなしに遭遇すると境界条件が気になるような処理が集まっていて嬉しい……そういう実用的なライブラリを目指す。

どうぞご利用ください。