ECMA262のIteration protocolsで遅延評価するIteratorを作る

これは何?

ECMA262 6th以降にはiteration protocols(と呼ぶべきもの)が導入されている、というのは皆さんもちろんご存知のとおりだと思います。これを使ってIterator<T>.next()が呼ばれるまでmap()などを実行しない(lazy evaluationする)Iteratorを作ってみようという話です。Rustのstd::iter::IteratorとかC#LINQとかが似たような挙動をしますよね。

今後、巨大なオブジェクトやリストに対してのIteratorが提供・出現した場合、毎回のように即時評価でfor-ofしたりArray.from()するなどは現実的ではないでしょう。以下のように書くことも出来ますが、

/**
 *  @template T, U
 *  @param {Iterable<T>} src
 *  @param {function(T):U} mapfn
 *  @return {Iterable<U>}
 */
function map(src, mapfn) {
  const iter = src[Symbol.iterator]();
  const next = function () {
    const { done, value } = iter.next();
    if (done) {
      return {
        done,
      };
    }
    const result = mapfn(value);
    return {
      done: false,
      value: result,
    };
  };

  return {
    [Symbol.iterator]() {
      return {
        next,
      }
    }
  };
}

const inifinityList = [...];
map(inifinityList);

bind operator proposalがacceptされないことにはa( b ( c() ) )のように書いていくケースの方が多くてダルい。俺はbind operatorの行く末を気にせずにArray.prototype.map()のようにメソッドチェーンで書いていきたいんだ、その気持ち、わかります。

というわけで、ものは試しにiterator protocolに則る形で実装してみましょう.

Proof Of Concept

型がややこしいのでTypeScriptで書いてみました。だいたいこんな感じ。どうせES6に型アノテーションつけた程度に書いてるので、適当にアノテーション落とせば動きます。

ちなみに基本設計はRxJS v5を元にしています。IterableとObservableでDualityだ! だからというわけではないですが、評価結果のキャッシュはopt-inになっています(cache())。安易にキャッシュしてしまうと、mapやfilterのコールバック関数がDate.now()などの副作用に依存している場合にバグってしまうので、明示的に使用する前提にしてます。

実用したい場合

大抵の場合は自分で再実装するか、Interactive Extensions (for JavaScript)あたりを待つか、新しい何かが出てくるのを待てば良いのではないでしょうか。

他にも、

などがありますが、2016年3月3日現在の安定版ではiteration protocolには対応してない模様ですが、今のところはES6~のIteratorが必須になるようなケースはそこまで多くはないと思っているので、これらを使うなどで凌ぐというのも現実的だと思います。