前回の方向で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);
}
}
# 文章を書くのが滅多にないためか、文体がコロコロ変わっちゃいますな。。