Ada風の非同期処理をJavaScriptで行うためのJs_Randezvousを作ってみた

JavaScirptでもランデブー風の処理が書けるように作ってみた。

cho45さんのJSDeferredからアイデアだけちょこっと貰って、コードは0から全部書きました。

まず、next()を沢山書かなければ行けないのが面倒なので引数に関数を並べる形に変更しました。さらに、JSDeferredはDeferredオブジェクトが状態を持てないので、そのままだとランデブーするための処理が書けません。そこでコンテクストを持つオブジェクトを持ち回るように実装して、そのオブジェクトが特異メソッド的にエントリを持つようにしています。

現時点でAdaの持つTaskingの8割は実装できているはずです。残ってるのはgurdsとselect delay abortとrequeueぐらいでしょうか。

また今のところ、Child deferredに対応するものが無いので、内部で再帰させる処理などが書けません。これはちょっと変更すれば何とかなると思うので、後で実装したいと思います。

とりあえず作ってみたデモでは二つのTask、t1とt2を作って、t1がひたすらacceptする側で、t2がcallしています。callって何だよとAdaプログラマの方は思うかもしれませんが、Entry Callのcallです。

一見無駄に関数として処理を区切っているところは、合間にUIスレッドに処理を返しているところです。無くても構いませんが、有るとUI側がスムーズ見えます。

サンプルコードは下みたいな感じ。まずはt1。ひたすらエントリの受付をします。

t1 = new Task(
    function(v) {
        print(1, "start.");
        print(1, "got an argument, '" + v + "'.");
    }, function() {
        print(1, "try to pass a variable to next statemtnt, "
              + "set 'Ada' to this.x1 .");
        this.x1 = "Ada";
    }, function() {
        print(1, "got a variable from the previous statemtnt, '"
              + this.x1 + "'.");
    }, function(v) {
        print(1, "the argument is still alive, '" + v + "'.");
    }, function() {
        print(1, "wating for entry1 called.");
    },
    accept(
        "entry1", function(msg) {
            print(0, msg);
        }
    ), function() {
        print(1, "randezvous at entry 1 completed.");
    }, function() {
        print(1, "waiting for entry2 which takes 2,000ms to complete.");
    },
    accept(
        "entry2", function(msg) {
            print(0, msg);
        },
        delay(2000)
    ), function() {
        print(1, "wating for entry3 called, using a syntax sugar.");
    }, function entry3(msg) {
        print(0, msg);
    }, function() {
        print(1, "randezvous at entry3 complated.");
    },
    function() {
        print(1, "wait 4,000ms not to accept t2's call.");
    },
    delay(4000),
    function() {
        print(1, "waiting entry4 for 0ms.");
    },
    select(
        function entry4(msg) {
            print(0, msg);
        },
        delay(0, function() {
                  print(1, "randezvous at t1.entry4 timed out.");
              })
    ),
    function() {
        print(1, "wating for entry5 or entry6 called for 10,000ms.");
    },
    select(
        function entry5() {
            print(0, "randezvousing at t1.entry5 .");
            return true;
        },
        accept(
            "entry6",
            function(msg) {
                print(0, msg);
                print(0, "takes 3,000ms.");
                return true;
            },
            delay(3000)),
        delay(10000, function() {
                  print(1, "randezvous at entry5/entry6 timed out.");
                  return false;
              })
    ),
    function() {
        if(this.__prev)
            print(1, "randezvous at entry5/entry6 completed.");
    }, function() {
        print(1, "completed.");
    }
);

次にt2。こちらはひたすらエントリーの呼び出しをする側。

var t2 = new Task(
    function() {
        print(2, "start in 3 secs.");
    },
    delay(3000),
    function() {
        print(2, "start.");
    }, function() {
        print(2, "calling t1.entry1 .");
    },
    call(
        t1.entry1,
        ["randezvousing at t1.entry1 ."]
    ), function() {
        print(2, "randezvousing at t1.entry1 completed.");
    }, function() {
        print(2, "call t1.entry2 in 4 secs.");
        this.i = 4;
    },
    loop(
        function() {return this.i > 0;},
        function() {
            print(2, "left: "  + this.i-- + " sec(s).");
        },
        delay(1000)
    ),
    call(
        t1.entry2,
        ["randezvousing at t1.entry2 ."]
    ),
    function() {
        print(2, "randezvouse at t1.entry2 completed.");
    }, function() {
        print(2, "calling t1.entry3 .");
    },
    call(
        t1.entry3,
        ["randezvousing at t1.entry3 ."],
        function() {
            print(2, "randezvousing at t1.entry3 completed.");
        }
    ),
    function() {
        print(2, "calling t1.entry4 for 2,000ms");
    },
    select(
        call(
            t1.entry4,
            ["randezvousing at t1.entry4 (this message never shown)"]),
        delay(2000, function() {
                  print(2, "aborted to call t1.entry4.");
              })
    ),
    function() {
        print(2, "completed.");
    }
);

2つ有るボタンはそれぞれt1のエントリーを手動で呼び出すための物です。t1が途中でentry5かentry6、もしくは10秒でタイムアウトのselect待機を行うので、ボタンを予め押しておくか、あるいはエントリーの受け付けが開始されてから押してみると、そこから先の処理が進むようになります。10秒経ってしまうとt1は自動で受付を諦めて先に進むので、押さなくても問題有りません。enty5のようにイベントリスナとして直接登録するのもよいですし(レシーバは気にしなくて平気です)、entry6のようにちょっと凝った呼び方もできます。

$("#call_entry5").click(t1.entry5);
$("#call_entry6").click(
    function() {
        var input = $(this);
        input.attr("value", "Wating for randezvous at t1.entry6");
        t1.entry6(
            ["randezvousing at t1.entry6 ."],
            function() {
                input.attr("value", "Randezvous at t1.entry6 completed");
            },
            function() {
                input.attr("value", "Randezvousing at t1.entry6");
            }
        );
    }
);