Stream.as

2008.7.14

前回の方向でStreamを再実装してみる。ただしそのままだとつまらないのでStreamをオブジェクト風にしてみる。nullを空ストリームを表すために使ってしまうとメソッドチェーンに失敗する可能性が出てくるので、StreamのサブクラスであるNullStreamを定義し、メソッド呼び出しに対して何もしないよう実装する。

enum(0, 100).filter(function():Boolean {return false;}).forEach(trace); // => (何も起きない)
trace(enum(0, 100).filter(function():Boolean {return false;})); // => [object NullStream]

([]).forEach(trace);と同様な挙動と言われれば当たり前と思うかもしれない。

今のところ最低限のことしか実装してないけど、やはりこっちのアプローチの方があっちよりシンプルかも・・?
勉強ため、ソースの中で解説してみる:

//Stream.as
package masda.stream {
  public class Stream {
    private var _car:Object;
    private var _cdr:Function;

//Streamの実体は、先頭要素(car)と残りのStreamを返す関数(cdr)。
    public function Stream(car:Object, cdr:Function/*delayed object*/) {
      _car = car;

//毎回cdrを評価するのはコストがかかるので、
//memoProcで元の関数の戻り値をキャッシュする関数を得る(後述)。
      _cdr = memoProc(cdr);
    }

//carはそのまま返す。
    public function get car():Object {
      return _car;
    }

//cdrはアクセスされて初めて評価される。
    public function get cdr():Stream {
      return _cdr(); //force
    }

//forEachは終端を表すnilでない限りcdrを辿り、carをコールバックに渡す。
//再帰で書くとスタックオーバーフローするので反復で書くことにする。
    public function forEach(proc:Function):void {
      var s:Stream = this;
      while (s != nil) {
        proc(s.car);
        s = s.cdr;
      }
    }

//StreamからArrayにするには、戻り値にする配列にバインドされたpushをforEachに渡すだけ。
    public function toArray():Array {
      var ary:Array = [];
      forEach(ary.push);
      return ary;
    }

//要素の参照もforEachの要領で行う。
    public function ref(n:uint):Object {
      var s:Stream = this;
      while (s != nil && n > 0) {
        s = s.cdr;
        n--;
      }
      return s.car;
    }

//mapは、carにそれを写像したものを置き、
//cdrが要求されたときに残りをmapしたStreamを返すようなStreamを返す。
//自身がnilならnilを返す。
    public function map(proc:Function):Stream {
      if (this == nil) return nil;

      return new Stream(proc(car), function():Stream {
        return cdr.map(proc);
      });
    }

//filterは、carに述語を満たす要素を置き、
//cdrが要求されたときに残りをfilterしたStreamを返すようなStreamを返す。
//スタックオーバーフロー対策のために、述語を満たすまでwhileによる要素のスキップを行う。
    public function filter(pred:Function):Stream {
      var s:Stream = this;
      var e:Object;
      while (s != nil) {
        e = s.car;
        if (pred(e))
          return new Stream(e, function():Stream {
            return s.cdr.filter(pred);
          });
        s = s.cdr;
      }
      return nil;
    }

//メモ化関数は、引数に指定した関数の戻り値をキャッシュするような関数を返す。
    private static function memoProc(proc:Function):Function {
      var alreadyRun:Boolean = false;
      var result:Object = false;
      return function():Object {
        if (alreadyRun) return result;

        result = proc();
        alreadyRun = true;
        return result;
      }
    }
  }
}
//nil.as
package masda.stream {
//nilはNullStreamの唯一のインスタンスとする。
  public var nil:NullStream = NullStream.getInstance();
}
//NullStream.as
package masda.stream {
//NullStreamはStreamのサブクラス。
  public class NullStream extends Stream {
    private static var instance:NullStream;

//NullStreamはシングルトン。
    public static function getInstance():NullStream {
//インスタンスを遅延初期化。
      instance = instance || new NullStream(new PrivateKey());
      return instance;
    }

//carにnullを置き、Stream#refでインデックスが範囲外のときにはnullが返るようにする。
//cdrが要求されたら自身を返すようにする。
    public function NullStream(key:PrivateKey) {
      super(null, next);
    }

    private function next():Stream {
      return this;
    }
  }
}

//このファイル内でのみ可視なクラスを、コンストラクタをこじ開ける鍵として使う。
class PrivateKey {};
//list.as
package masda.stream {
//引数を要素とするストリームを作成する関数。
  public function list(...args):Stream {
//引数が底つきたらnilを返して終わりを告げる。
    if (args.length == 0) return nil;

//引数は、cdrにアクセスされるたびにそのcarに消費されて短くなる。
    return new Stream(args.shift(), function():Stream {
      return list.apply(null, args);
    });
  }
}

//enum.as
//startから始まり、stopを含む数までをstepの間隔で並べたストリームを返す。
//stopを省略すると無限リストになる。
package masda.stream {
  public function enum(start:Number = 0, stop:Number = NaN, step:Number = 1):Stream {
    function numbersStartingFrom(n:Number, step:Number):Stream {
      return new Stream(n, function():Stream {
        return numbersStartingFrom(n + step, step);
      });
    }

    function enumerateInterval(start:Number, stop:Number, step:Number):Stream {
      if ((step > 0 && start > stop) || (step < 0 && start < stop)) return nil;

      return new Stream(start, function():Stream {
        return enumerateInterval(start + step, stop, step);
      });
    }

    return isNaN(stop) ?
      numbersStartingFrom(start, step) :
      enumerateInterval(start, stop, step);
  }
}

# 文章を書くのが滅多にないためか、文体がコロコロ変わっちゃいますな。。