遅延評価の楽しみ

最近のプログラミング言語はイテレータを備えている。配列に対して逐次的な遅延処理が行えるものだ。自分が知るだけでもいくつかある。

  • Pythonの map()filter()
  • C#の IEnumerable<T>
  • Rustの iter()

JavaScriptの filter()map() はどういうわけかイテレータではなく、配列を返すようになっている。おそらく、イテレータやジェネレータが導入される前に反復処理メソッドとしての役割を期待されて導入に至ったからだろう。

反復処理メソッドは要素数が小さいうちは極めて速く動作する。Pythonの内包表記も同様だ。しかし、遅延評価ができないので、無限のリスト(これはHaskellで有名になった話題である)を扱うことができないし、そのデータを使うにせよ使わないにせよ、すべての値が評価されて新しい配列を生成する。

まあ実際のアプリケーションで、無限やとてつもなく大きな要素数を扱うことがあるかと言われるとあまりないが、たまにそれらをまともに扱うことが馬鹿馬鹿しくなるような状況はある。

Rustのイテレータを再現

最近よくRustを書いていることもあり、JavaScriptでRust-likeなイテレータ操作を作ってみようと思い立ち、その前に似たようなことをしている人がいないか少し検索してみたところ、そのものズバリのものは見つけられなかったが、試験的に実装しているRust Iterators in JavaScriptを見つけることができた。

これはとてもいい例だったが、私にとっては少々規模が小さすぎたので、実際にRustのソースコードを参考に書いてみたが、これがまたイテレータへの知識が深まるいい経験になった。基本的に next() が実装されていればよいというのはかなりいいコンセプトである。

3日ほどコネコネした結果、最終的に以下のようなコードが書けるようになった。試しに作ってみたものなので、未対応のメソッドもある。

[1, 2, 3, 4, 5].iter().skip(2).eq([3, 4, 5].iter())

実際のところ組込みの map()filter() で十分ではないか?という意見に対してはまったくそのとおりと言わざるを得ないが、そのうちこれが役に立つときが来るかもしれない。

それよりも、これを実装するにあたってJavaScriptでClonableでEquatableでComparableな状況を作り出したほうが興味深く、実現のために組込みオブジェクトのオーバーライドもしている。これについては賛否両論あるだろうが、個人的にはそのくらいしないとJavaScriptはまともに使えないのではないと考えている。

そういえば

C#の IEnumerable<T> はちょっと曲者だ。すべての列挙可能型はこれを実装していてアップキャストができるようになっている。つまり列挙可能型は文脈上 IEnumerable<T> が要求されていれば、それを与えることができる。List<T>IReadOnlyList<T>Dictionary<TKey, TValue> もみんな IEnumerable<T> になれる。ここまでは問題ない。

しかし、Linqメソッドを呼び出した瞬間に IEnumerable<T> は少し危険な状況になる。すべてが遅延評価になるので、期待した動作にならなくなるかもしれないからだ。以下は私が実際に書いて失敗したコードだ。

var userList = users.Select(x => new { Id = Guid.NewGuid(), Name = x.Name, Age = x.Age });
var userListAdult = userList.Where(x => x.Age >= 20);
var userListNotAdult = userList.Where(x => x.Age < 20);

Guid.NewGuid()ランダムにUUIDv4を払い出すメソッドだ。そしてイテレータは遅延評価をする。そのため、評価ごとに異なるUUIDが生成されることになる。正しく動かすためには1行目の最後で .ToList() をしておく必要があり、これによってUUIDを確定しておける。

そういう危険な側面はありつつも、依然 IEnumerable<T> は便利である。受けられる型が多くなるし、不要なリスト型変換も不要になる。そもそもC#にはリスト派生型が多すぎるのである。そのため、メソッドやコンストラクタの引数は可能な限り IEnumerable<T> であることが望ましい。