PerspectiveProjection

2009.2.3

自分が担当してるSWFがルート(stage直下のドキュメントクラス)として起動されるケースと、子として読まれるケースがある案件で、パースの見た目が異なってハマった。。PerspectiveProjectionがnullじゃないDisplayObjectがネストされたとき、どういう扱いになるんだろう??

3D使うのはそこだけだったので、addChildされたとき上方のperspectiveProjectionを全てnullにしつつ、ルートの表示オブジェクトのみ値を設定して結果を合わせた。けどなんか気持ち悪い。。

var o:DisplayObject = this;
while (o.parent != stage) {
  o.transform.perspectiveProjection = null;
  o = o.parent;
}

リキッドの場合は、プロジェクションセンターも更新しないと意図通りにならないかも・・。

private function onResize(evt:Event):void {
  var proj:PerspectiveProjection = root.transform.perspectiveProjection;
  var pt:Point = proj.projectionCenter;
  pt.x = stage.stageWidth * 0.5;
  pt.y = stage.stageHeight * 0.5;
  proj.projectionCenter = pt;
  root.transform.perspectiveProjection = proj;
}

GC判定再び

2008.12.20

GCされたかテストするクラスの改良版。

最後の参照を消すとき、

_instanceVar = null; //これでいつか解放されるはずだが・・

と書いているところを、

//常に戻り値のnullが代入されるが、同時に漏れを確認
_instanceVar = GC.shouldBeGarbage(_instanceVar); // 以降、ゴミとして回収されるべき!

と書くことにします[*注]。これで参照の消し忘れがあれば、直後にエラーになってくれます。多分。

ちなみにFlexでは子SWFがアンロードされると、以下のように表示されるのでこれの出番は少ないかもしれません。

[SWF のアンロード]Users:masuda:Documents:Flex Builder 3:Test:bin-debug:child.swf

以下にいくつかサンプルを挙げます。
ドキュメントクラスのコンストラクタに書かれているコードだと思って下さい。

ローカル変数はGCされる:

var o:DisplayObject = new Sprite();
GC.shouldBeGarbage(o); //=> GC succeeded: 4489216 => 4771840

インスタンス変数に退避したため延命され、GCされない:

var o:DisplayObject = new Sprite();
_instanceVar = o;
GC.shouldBeGarbage(o); //=> Error

表示リストに残っているため、GCされない:

_instanceVar = new Sprite();
addChild(_instanceVar);
//removeChild(_instanceVar); //この行が必要だった
_instanceVar = GC.shouldBeGarbage(_instanceVar); //=> Error

ローカル変数はクロージャに延命され、クロージャはstageに延命されるのでGCされない:

var o:DisplayObject = new Sprite();
addChild(o);
removeChild(o);
GC.shouldBeGarbage(o); //=> Error
//o = null; //この行が必要だった
stage.addEventListener(
  Event.ENTER_FRAME,
  function(evt:Event):void {/*どの変数も参照してなくても害*/}
);

クロージャとメモリリークについての親切な解説はこちら:
http://www.imajuk.com/blog/archives/2008/04/post_3.html

以下、ソースコードです:

package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.net.LocalConnection;
  import flash.system.System;
  import flash.utils.Dictionary;

  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 shouldBeGarbage(value:Object):* {
      try {
        //テストは1フレ後だが、この時点のスタックトレースがほしい
        throw new Error('Probably, it is not garbage:' + value);
      } catch (e:Error) {
        var dict:Dictionary = new Dictionary(true);
        dict[value] = true;
        value = null;

        var before:uint = System.totalMemory;
        GC.start();

        var err:Error = e;
        sprite.addEventListener(Event.ENTER_FRAME, function(evt:Event):void {
          evt.target.removeEventListener(evt.type, arguments.callee);
          for (var key:Object in dict) throw err;
          trace('GC succeeded:', before, '=>', System.totalMemory);
        });
      }
      return null;
    }
  }
}

[*注] 以下の例は、全ての参照を消すのに先だってshouldBeGarbageを行うためGCが成功しそうにありませんが、実際のところ、このように書いてもテストをパスします。

GC.shouldBeGarbage(_instanceVar);
_instanceVar = null;

恐らく、LocalConnectionによる強制GCの発動は即座に行われずに、一度Flash Playerに制御を戻した後行われているのだと想像しています・・。そうなると、shouldBeGarbage(x)の意味合いとしては、「現在実行中のコードを抜けた後には、xはゴミとなり回収されるべきだ」といったところでしょうか。

las3r (2)

2008.12.5

las3r、Clojure(むしろLisp)がよく分からないまま手探りで弄っています。便利だと思ったのが、アリティによる多重定義と、暗黙のdestructuring-bind(アンパック代入みたいなもん?)。

以下は0〜2個の引数を取るMath.random:

(defn rand
  ([] ((. Math random)))
  ([n] (* n (rand)))
  ([a b]
    (if (< a b)
      (+ a (rand (- b a)))
      (+ b (rand (- a b))))))

(rand -1 1) ;=> -0.689156367443502

要は引数のパターンに応じた振り分けと同時に変数束縛されるようですが、これが再帰関数の場合には特に便利で、例えばリストをペアに区切ったリストにして返す手続きは:

(defn pairs [l]
  (apply
    (fn
      ([x] (list (list x)))
      ([x y] (list (list x y)))
      ([x y & z] (cons (list x y) (pairs z))))
  l))

(pairs '(1 2 3 4 5)) ;=> ((1 2) (3 4) (5))

ところで、las3rでは{:x 200 :y 400 :time 0.5}と書くと内部的にはMapというオブジェクトになってしまうようなので、Tweener.addTweenの第二引数に渡せません。MapからObjectへ変換するそれらしき関数も見つからなかったので、試しにObjectリテラルに近い書き方ができるマクロを書いてみると:

(defmacro obj [& elts]
  (let [o (gensym)]
    `(let [~o (new Object)]
      ;;fnやletのパラメータはdestructuring-bindされる雰囲気。
      ~@(map (fn [[var exp]] `(set! (. ~o ~var) ~exp)) (pairs elts))
    ~o)))

(macroexpand-1 '(obj x 200 y 400 time 0.5))
;=>
;(las3r/let [G__1378 (new Object)]
;  (set! (. G__1378 x) 200)
;  (set! (. G__1378 y) 400)
;  (set! (. G__1378 time) 0.5)
;  G__1378)

(obj x 200 y 400 time 0.5) ;=> [object Object]

これで以下のようにだらだらと書かなくて済みます。

(. Tweener
  (addTween
    movie-clip
    (let o (new Object)
      (set! (. o x) 200)
      (set! (. o y) 400)
      (set! (. o time) 0.5)
      o)))

クリックすると画面上をランダムに移動する四角形はこんな雰囲気:

(import '(flash.display Sprite Graphics)
        '(caurina.transitions Tweener))

(def s ((. *stage* addChild) (new Sprite)))

(doto (. s graphics)
  (beginFill 0x0000ff 1)
  (drawRect 0 0 100 100)
  (endFill))

(. s
  (addEventListener "click"
    (fn []
      (. Tweener (addTween
        s
        (obj x (rand (. *stage* width))
             y (rand (. *stage* height))
             time 0.5))))))

最後に衝撃のfib(25)の時間比較:

  • ABC
    => 88 msec
  • AS3
    => 125 msec
  • las3r
    => 5465 msec
  • SICP4.1.7をASに移植したもの(比較用)
    => 11255 msec

なるほど・・・。
まだきっと伸びしろはありますよね!
速くなるといいなぁ。。

なお、計測の対象は以下の関数:

AS3:

private function fib(n:uint):uint {
  return (n < 2) ? n : fib(n - 1) + fib(n - 2);
}

ABC(hxasmを使用):

var ctx:Context = new Context();
ctx.beginClass("Main");
var tint:Index = ctx.type("int");
var m:* = ctx.beginMethod("fib", [tint], tint);
m.maxStack = 3;
ctx.ops([
  OpCode.OReg(1),
  OpCode.OSmallInt(1),
]);
var j:Function = ctx.jump(JumpStyle.JGt);
ctx.ops([
  OpCode.OReg(1),
  OpCode.ORet,
]);
j();
ctx.ops([
  OpCode.ODecrIReg(1),
  OpCode.OThis,
  OpCode.OReg(1),
  OpCode.OCallProperty(ctx.property("fib"),1),
  OpCode.ODecrIReg(1),
  OpCode.OThis,
  OpCode.OReg(1),
  OpCode.OCallProperty(ctx.property("fib"),1),
  OpCode.OOp(Operation.OpAdd),
  OpCode.ORet,
]);
ctx.finalize();

var o:Output = new Output();
Writer.write(o, ctx);
var swf:ByteArray = o.getBytes();

var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(evt:Event):void {
  var klass:Class = ApplicationDomain.currentDomain.getDefinition('Main') as Class;
  callback(new klass().fib); //この引数がコンパイル済みfib
});
loader.loadBytes(swf, new LoaderContext(false, ApplicationDomain.currentDomain));

las3r:

var rt:RT = new RT(stage, new OutputStream(trace), new OutputStream(trace));
rt.loadStdLib();
rt.evalStr(
  '(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) fib',
  callback //fnを評価するとコンパイル済みfibがcallbackに渡る
);

SICP4.1.7をASに移植したもの:

var fib:Function =
  eval("(define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) fib");

/*evalの定義は長いので略*/

las3r

2008.12.2

clojureというJVM向けLisp方言があるらしいのですが、それのAVM2向けコンパイラがAS3で書かれています。
http://github.com/aemoncannon/las3r/wikis

構文的な違いが結構あり戸惑ったのですが、こちらを見ればだいたいつかめると思います。

Schemeじゃないから当然かもしれませんし、ABC的に実装に難があるからかも分かりませんが、残念ながら:

  • 末尾最適化なし (末尾再帰にはrecurを使えばスタックを消費しない)
  • 継続なし

しかし、それを差し引いてもASにない以下の特徴を楽しむことができます:

  • マクロが使える
  • 動的コンパイル

しかも、Emacs向けにlas3r-modeが用意されていて、起動中のSWFに対し修正を動的に反映できます。
以下手順:

  1. etc/las3r-mode.elをEmacsのロードパスへ配置
  2. もしなければ、以下もDLしてロードパスへ配置
    http-get.el, http-cookies.el, http-post.el
  3. .emacsに以下を追記
    (autoload 'las3r-mode "las3r-mode" "Major mode for las3r." t)
    (add-to-list 'auto-mode-alist '("\\.lsr$" . las3r-mode))
  4. src/lsr/boot.lsrの最終行、;;(connect-to-eval-pipe)をコメントインしておく
  5. etc/eval_pipe.rbを起動しておく
  6. 対象のSWFを起動しておく
  7. Emacsにて*.lsrの編集中にF5 (すると、バッファ全体がevalされる)

この手の試みがじわりじわりと増えてきているような気がしますね・・。
今後が楽しみです。

関連メモ:
http://hg.mozilla.org/tamarin-central/file/d92e466aed84/esc/src/
元?

http://eval.hurlant.com/
上記のAS3版だと思う。las3rはこれの一部を使っている。

http://ariyan-harhid.cocolog-nifty.com/blog/2008/10/tamarin-esc-c-2.html
上記の改良版。実行時エラー情報を詳しく教えてくれたりetc

http://happyabc.org/
OCaml製のSchemeコンパイラ

http://www.bitbucket.org/SumiTomohiko/actionpython/overview/
これは・・?

http://haxe.org/com/libs/hxasm
haXeのABCアセンブラ

http://www.sephiroth.it/weblog/archives/2008/07/expressions_evaluation_at_almost_nati.php
上記のAS3版を基礎としたシンプルな電卓デモ

http://www.anotherbigidea.com/javaswf/avm2/AVM2Instructions.html
見やすい

http://code.google.com/p/as3c/
インラインアセンブラ

jp.nium.display

2008.8.4

jp.nium.displayをちらみしたメモになります。開発中のrev1837の時点を見てフライングで書いているので、予告なしに変更され得る層を紹介しようとしているのかもしれませんし(というのも書いている最中にlistenOnStageが消えたからです!)、逆に周知のことを紹介しているのかも分かりませんが、痒いところに手が届いているあまり興奮を禁じ得ませんでした。その点ご了承下さい。

実は最近になってようやくProgressionを勉強しています。ver3からの入門です(汗)。3本柱の一つであると聞くキャストですが、今まで表示オブジェクトのサブクラス化を好んで行わない(= リンケージしないでインスタンス名つけて操作する)スタイルで制作していたという極めて個人的な理由で食わず嫌いしていたのですが、その考えはすぐに変わることになりました。

リンケージの手間を惜しむ私のような人間には、リンケージ一括設定(特に接尾辞プロファイル!)が以前から提供されていたようですし、開発予定の画面ではなんかキャストのテンプレートを自動生成してくれそうな雰囲気すら漂わせているように見えますが(気のせいでしょうか?)、もしそのようになればキャストのサブクラスを作る負担はかなり減ることになるので期待が膨らんでしまいます。さらにキャストはonCastXXXの処理中に追加したコマンドを同期化してくれるだけかと思っていたものの、実はjp.nium.display系の便利機能を継承していたことを知り、主な機能をチェックしてみようと思ったわけでした。

IEventIntegrator

removeAllListeners(completely:Boolean = false):void
restoreRemovedListeners():void
これら二つを用いて、イベントの配信停止と再開が手軽に実現できるようです。ただし、addExclusivelyEventListenerで追加された場合この限りではないようです。

IExDisplayObject

このインターフェースを実装しているクラスのインスタンスは、jp.progression.casts.*の以下の3つのパッケージ関数によって表示ツリーの階層を問わず簡単に取得できるようです。

getInstanceById(id:String):DisplayObject
表示オブジェクトにidを設定しておくと、階層問わず参照できるようです。getElementByIDみたいですね。

getInstancesByGroup(group:String, sort:Boolean = false):Array
表示オブジェクトにグループ名を設定しておくと、階層問わず集められるようです。

getInstancesByRegExp(fieldName:String, pattern:RegExp, sort:Boolean = false):Array
任意のフィールドで正規表現にマッチした表示オブジェクトを階層問わず集められるようです。例えば、ボタンごとにインスタンス名をつける代わりに以下のように参照することもできそうです。

//同一シンボルを1つしか配置しないため、クラス名からインスタンスが一意に決まる
CastButton(getInstancesByRegExp('className', /^AboutButton$/)[0])
.sceneId = aboutScene.sceneId;

追記: 先ほど試してみたところ、これらの3つのメソッドはaddChildしてステージに追加しなくても使えるみたいです。ということはid設定したら自己責任でexDisplayObject.id = null;などとしてコレクションから消さないとGCされないのかな・・?

setProperties(props:Object):DisplayObject
プロパティの一括設定を行えるようです。CastSpriteの表示アニメーション以前に行う座標などの初期化をすっきりとまとめられそうです。

ExDisplayObject

toBitmapData(transparent:Boolean = true, fillColor:uint = 0xFFFFFFFF):BitmapData
表示オブジェクトの内容をBitmapData化したものを取得します。

以上の操作は、ExLoader, ExMovieClip, ExSprite, ExTextFieldなどで利用できる感じです。このうち、コンテナ系のものであれば以下も利用できる感じです。

IExDisplayObjectContainer

children : Array
DisplayObjectContainerの子の走査は面倒でしたが、これで随分楽になりそうです。IExDisplayObjectContainerは、必ずしもインデックス値が連続している必要はないため、あらかじめ大きな値を設定しておいて最前面を確保したいときなどに便利そうです。となれば、走査するにあたってnull要素を除いた子のリストが欲しくなるでしょうが、こいつがそれを提供してくれるっぽいです。

contains(child:DisplayObject):Boolean
指定された表示オブジェクトを含むか、自身であればtrueを返します。孫、ひ孫・・とツリー全体をテストしてくれるようです。

removeAllChildren():void
全ての子を削除します。地味に便利です。

最後に、インスタンス化できるものでユニークだなと思った機能に以下があります。

ExSprite

isDragging : Boolean
startDrag()を使用中か否かを返します。

ExMovieClip

isPlaying : Boolean
ムービークリップの再生ヘッドが移動中か取得します。

repeat : Boolean
ループ再生するかを取得または設定できるようです。

switchAtPlaying():void
再生と停止を切り替えます。

ExTextField

appendTextAtCaretIndex(newText:String):void
テキストフィールドのキャレット位置に文字列を挿入できるみたいです。

displayパッケージの機能拡張は継承の苦しい裂目が見えてきて大変な部分だと想像しますが、ここまで使い勝手の優先を貫いていることに感心してしまいました。おかげで、各々の表示キャストはDisplayObjectと同一視できつつ、一方でIExDisplayObjectとしての顔も持つようです。しかし、これもProgressionのごく一部に過ぎないと思うと驚異的ですね!

非同期処理いろいろ

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; //呼び出し回数が多いものから表示