非同期処理いろいろ

2008.7.24

去年頃から、シングルスレッドかつイベントドリブンなFlash環境で散らかりがちな非同期処理を、すっきりと書けるようにするための試みが多く見受けられるようになりました。それらの特徴をざっと調べてみたメモになります。先駆者達のやり方を広く知り、あわよくば何か洞察を得たいからであって、各ライブラリの優劣を独断と偏見で決定するような主旨ではありません。それからプログラム勉強する前にまず日本語の勉強しろっていうくらい思いやりのない説明が延々続きますが、万が一誰かの参考になればと思い載せておくことにします。

各ライブラリの特徴をつかむため、以下のポイントについて共通で見ていくことにします。
more...

HoC spectrum

2008.7.19

House of Cardsのデータで遊んでみました:
http://www.metaphor.co.jp/2008/07/visualization-of-spectrum-radiohead-house-of-cards/

マウスを動かすとアングルが変わるのですが、顔を横から見たときにサウンドスペクトルになっています。
勢いに任せたひどいソースは以下:
source code

GoogleCodeで提供されているCSVそのままだとちょっと重いので、上のソースに含まれているcsv2binary.rbを使っていろいろと都合のいいバイナリファイルに変換しています。具体的には:

  • 「頂点数, X, Y, Z, 明度, X, Y, Z, 明度, X, Y, Z, 明度・・・」 の繰り返しの並び
  • フレームを間引く (デモでは1フレ飛ばしでデータを作り、15fpsで再生)
  • 明度が低い頂点を捨てる (デモでは60以下を捨てたため髪の毛がない)
  • X, Y, Zは適当に16bitにおさめる
  • モデルの中心あたりに原点を移動

Flash側はそれをロードし、ByteArrayから頂点情報を読み出して描画という感じです。音に同期させるため、(フレームレートに依存しない)再生時間を元にByteArray中の目的とするフレームを指すpositionを計算しています。

追記:
いちから解説をするべきでしょうが、以下に非常にシンプルなコードが紹介されていたため割愛します!

RadioheadのプロモーションがGoogle Codeで行われている理由

さらに追記:
今改めて未読消化してみるとFlash版の挑戦者はたくさんおられたようです。日付を見るにとても仕事速い!

http://blog.r3c7.net/?p=209
http://nutsu.com/blog/2008/071720_radiohead.html
http://aquioux.blog48.fc2.com/blog-entry-446.html

迷わず頂点を減らした私はなんだか反則してしまったような気持ちです。。

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);
  }
}

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

stream-cons

2008.7.14

SICPのストリームの定義がかっこよくてしびれました。

例えば引数に指定した数から始まる整数列を返す手続きはSICPだと:

(define integers-starting-from n
  (stream-cons n (integers-starting-from (+ n 1))))

これをAS3でやると多分こんな感じになる:

function integersStartingFrom(n:uint):Stream {
  return new Stream(n, function():Stream {
    return integersStartingFrom(n + 1);
  });
}

自分が以前似たようなことをやったときは、同様の関数が以下のような醜い姿に:

function count(start:uint):Iterable {
  return new Iterable(function():Function {
    var i:uint = start;
    return function():uint {
      return i++;
    };
  });
}

遅延リストの構造を「次の要素を返す関数を返す関数」から「cdrが遅延評価されるconsセルを使ったリスト」にしただけでこれだけのシンプルさが得られるなら、あれをもう一度書き直してもいいような気がしています、またいつか!

以下余談:

AS3は特殊形式を扱えないので遅延評価させたい式はいちいち無名関数で包むことになりますが、まあそれは仕方がないとして、末尾最適化がない(ですよね?)のでちょろっと再帰しただけで即スタックオーバーフローするのが終わっています。従って、ストリームの操作の中にはwhileと代入の形に翻訳せねばならないものがあるのが残念なところです。

Bench.as

2008.7.12

普段使っている簡単なベンチマーク用のクラスを晒してみます。この手のやつってみんな自作のものを持っているか、もっといいものがあると思いますが、こういうやり方もあるということで・・。

早速使い方ですが、計測したい部分を以下のようにbeginとendで囲みます。

var ary:Array = [];

Bench.begin("loop");
for (var i:uint = 0; i < 500; ++i) {
  var sum:uint = 0;

  Bench.begin("subloop");
  for (var j:uint = 0; j < i; ++j) {
    Bench.begin("add");
    sum += j;
    Bench.end();
  }
  Bench.end();

  ary.push(sum);
}
Bench.end();

/*
                 time       rate      count
loop              830      1.000          1
subloop           826      0.995        500
add               223      0.269     124750
*/

そうすると、コメントにあるような表がトレースされます。表の1列目はbeginの引数に与えたラベル、2列目がミリ秒単位の処理時間、3列目が処理時間の割合、4列目が呼び出された回数になります。beginの引数を省略すればその部分の結果は出力しないようにもできます。また、ネストした場合、トップレベルのベンチマークが終わったときにまとめて出力されるので、他のtrace文が入っていても結果が混ざって見にくくなる心配はありません。

これにはもう一つ使い方があって、timesというメソッドにコールバック関数と回数を指定して計ることもできます。endやtimesは計測した時間を返すので、必要であれば以下のようにしてどの程度パフォーマンスが改善されたかを求めることができます。

function func() {
  var a = 1, b = "", c = {};
  a + b + c;
}

function typedFunc():void {
  var a:Number = 1, b:String = "", c:Object = {};
  a + b + c;
}

var count:uint = 100000;
Bench.times(null, function():void {
  var t0:uint = Bench.times("before", func, count);
  var t1:uint = Bench.times("after", typedFunc, count);
  trace(Math.round(100 * (t0 / t1 - 1)) + "%の高速化");
});

/*
39%の高速化
                 time       rate      count
before            759      0.582          1
after             545      0.418          1
*/

もし、ラベル名が長くて表が崩れてしまうときは、列の横幅を大きめに設定しておくこともできます。

Bench.columnWidth = 20;

ソースですが、ブログに貼るとなんか記号が化けるのでこちらから。

追記:
いくつか気になる点があったので、本文中の例とソースを以下のような修正版に差し替えました。

  • ループ等で同じラベルを持つ計測が複数回行われた場合、表示を1行にまとめるようにした。
  • 出力する表にカラム名を加えた。
  • 出力する表に呼び出し回数の列を加えた。
  • 無名関数を指定した後にラベルが続くと見にくいので、timesの引数の順序を変えた。

さらに追記:
出力する表のソートキーを選べるようにしました。Bench.orderというプロパティに定数を代入する感じになります。

Bench.order = Bench.BEGIN; //実行が開始された順で表示 (デフォルト)
Bench.order = Bench.LABEL; //ラベルの辞書順で表示
Bench.order = Bench.TIME; //時間がかかったものから表示
Bench.order = Bench.COUNT; //呼び出し回数が多いものから表示

delayed list

2008.7.10

前回紹介した反復処理ライブラリは、mapやfilterなどの一部のメソッドで遅延リストを返すようにして部分的にストリームのパラダイムを取り入れていました。要素は要求に応じて計算されるまで存在しないのにも関わらず、利用者からはあたかもリスト全体が存在しているかのように見え、リスト演算でプログラムを表すことができます。

例えば、n番目の素数を見つけるプログラムは:

function isPrime(n:uint):Boolean {
  function smallestDivisor(n:uint):uint {
    return Iterable
      .range(2, Math.sqrt(n) + 1) //2から√nまでの数のうち
      .find(function(divisor:uint):Boolean { //nを割り切れる数が最小除数
        return n % divisor == 0;
      }) || n; //それがなければnが最小除数
  }
  return n == smallestDivisor(n); //最小除数が自身であれば素数
}

function primeAt(n:uint):uint {
  return Iterable
    .count(2) //2以上の整数のうち
    .filter(isPrime) //素数であるものの
    .slice(n - 1, n).toArray()[0]; //n番目
}

trace(primeAt(1000)); // => 7919

とストレートに書けます。toArrayのタイミングで評価が強制され、countからは必要な数だけ整数が供給されます。これがもし遅延リストでなく、通常の配列ならどうでしょうか?仮に「100以下の素数を求めるプログラム」という問題なら以下のようにすればいいだけです。

//符号なし整数の区間を配列で返す
function range(a:uint, b:uint):Array {
  var ary:Array = [];
  for (var i:uint = a; i <= b; ++i) ary.push(i);
  return ary;
}

range(2, 100).filter(function(e:uint, ...rest):Boolean { //引数を合わせるためにラップ
  return isPrime(e);
});

ところが、「素数のn番目を求める」プログラムを配列の演算で表すには、事前にいくつまでの整数を生成したら十分か見積もれない、正確に見積もれたとしたらそれこそが欲している答えになってしまいますから無理があります。

遅延リストを導入することでリストが利用できる機会が広がって楽しくなります。
# AS3の記事ではありますが、半ばSICPの勉強メモとなってしまいました。。

generator

2008.7.6

ActionScript3でPythonっぽい(?)外部イテレータを利用する話です。既出だったらどうしよう。。

http://www.be-interactive.org/index.php?itemid=40
http://www.be-interactive.org/?itemid=39

数年前、ActionScript神がRubyのEnumerableをASでやっていました。eachメソッドさえ実装すれば、様々な便利メソッドがミクシンで使えるようになるというものです。その後AS3になり、Array#mapとか標準で使えるようにはなったのですが、クロージャに要求する引数の数が多くてちょっと使いにくいと思います。

そこで以下のような反復処理ライブラリをやってみたくなるわけです。

  • Python的な外部イテレータで無限リストとか扱えるようにしてみよう。
  • 神に習って反復をbreakする方法を提供しよう。
  • 大クラス主義で作ってメソッドチェインを促進しよう。

more...

assertGC()

2008.7.5

ActionScript3にて特定のオブジェクトがGCされたかを確認する話です。既出だったらどうしよう。。

http://d.hatena.ne.jp/kkanda/20080406/p1

上記の記事は監視オブジェクトの生存数の変化を通知してくれるものみたいですが、一つのオブジェクトにつき一つのディクショナリを作れば特定オブジェクトのGCをフックできそうです。以下のような感じで:

GC.watch({}, function():void {trace("gc!");});

さらにGC強制発動と併用すると、参照がどこにもなくなってGCされるべきオブジェクトを表明する関数が考えられます。表明をパスすれば安心して作業が進められそうですね。使用例は以下のような感じに:

package {
  import flash.display.Sprite;
  import flash.events.MouseEvent;

  public class Main extends Sprite {
    public var o:Object;

    public function Main() {
      o = {};
      var item:GCItem = GC.watch(o);
      stage.addEventListener(MouseEvent.CLICK, function(e:MouseEvent):void {
        o = null; //ここをコメントアウトすれば表明に違反してエラー
        item.assertGC(); //ここでエラーが起きなければGCされたことになる
      });
    }
  }
}

GC.watch()の戻り値を取っておき、参照がなくなったと思われる時点でassertGCを呼んでやれば、メソッド内部で強制GCを試みて、もし回収されていなかった場合にエラーを送出します。もっとも、強制GCの手法が非公式らしいのでいつまで機能するかは不明です。

ソースは以下のような感じになります:

package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.net.LocalConnection;

  public class GC {
    private static var sprite:Sprite = new Sprite();

    public static function start():void {
      try {
        new LocalConnection().connect("foo");
        new LocalConnection().connect("foo");
      } catch (e:*) {}
    }

    public static function watch(value:Object, callback:Function = null):GCItem {
      var item:GCItem = new GCItem(value);
      value = null;
      sprite.addEventListener(Event.ENTER_FRAME, function(e:Event):void {
        if (!item.target) {
          callback && callback();
          e.target.removeEventListener(e.type, arguments.callee);
        }
      });
      return item;
    }
  }
}
package {
  import flash.utils.Dictionary;	

  public class GCItem {
    private var dict:Dictionary;

    public function GCItem(value:Object) {
      dict = new Dictionary(true);
      dict[value] = true;
    }

    public function get target():Object {
      for (var key:Object in dict) return key;
      return null;
    }

    public function assertGC():void {
      GC.start();
      if (target) {
        throw new Error("the object is not collected: " + target);
      }
    }
  }
}

一つ問題があって、最後の参照がなくなってから最低1フレーム経ってからGC.startしないと、GCの検出がうまかいかないような雰囲気です。従って、以下の例は期待に反して、表明に違反してしまいます。

var o:Object = {};
var item:GCItem = GC.watch(o);
o = null;
item.assertGC();

一方、以下のようにすれば期待通りに行きます。

var o:Object = {};
var item:GCItem = GC.watch(o);
o = null;
stage.addEventListener(Event.ENTER_FRAME, function(e:Event):void {
  item.assertGC();
  e.target.removeEventListener(e.type, arguments.callee);
});