ジンジャー研究室

長めのつぶやき。難しいことは書きません。

pegjs で Reactive Programming な Alt.js を作ってみた。

Rea10

たとえBackbone.jsのような優れたライブラリを使っていたとしても、Observerを実装しようと思ったらそれなりに面倒な記述をしなければなりません。怠惰な私にはそれが耐えられません。
なので、そういう事をする前提のAlt.jsを作ってしまえばいいんじゃないかと思いました。

というわけで、Rea10という言語を作りました。
https://github.com/jinjor/rea10

リアクティブプログラミング

リアクティブプログラミングについては、以下の記事でとても分かりやすく説明されているので、ここで説明する必要はないでしょう。
なぜリアクティブプログラミングは重要か。

関数型の文脈で語られるときはFRP(Functional Reactive Programming)と言ったりするそうです。

pegjs

JavaScript製の構文解析器です。
構文定義を書いて文字列をぶち込むと構文木を作ってくれます。

こちらも素晴らしい解説記事があるので説明は割愛します。
PegjsでAltjsを作る 第一回 Number型の表現

コンパイラ実装風景

ざっくりどんな感じかをつかむためには、コードをさらすのが一番だと思うので。

構文定義

なし

何故なしかというと、今のところJavaScriptの構文をそのまま利用してなんとかなっているからです。
というわけで、pegjs/examples/javascript.pegjs をそのまま利用します。

コード生成部分

大体次のような感じで、一つ一つの構文に対して「もしこの構文だったらこのコードを生成して返す」を再帰的にやります。

var compile = function(ast){
    var result
    if(ast.type == 'Program'){
        result = ast.elements.map(function(a){return compile(a)}).join('\n')
    }else if(ast.type == 'BinaryExpression'){
        var left = compile(ast.left);
        var right = compile(ast.right);
        result = 'new _Node().assign(' + left + ',' + right + ',' + 'function(a,b){return a' + ast.operator + 'b;})';
    }else if(ast.type == 'NumericLiteral'){
        result = 'new _Node(' + ast.value + ')';
    }else if(ast.type == 'StringLiteral'){
        result = 'new _Node("' + ast.value + '")';
    }else if(ast.type == 'VariableStatement'){
        result = ast.declarations.map(function(a){return compile(a)}).join();
    }else if(ast.type == 'VariableDeclaration'){
        result = 'var ' + ast.name + '=' + compile(ast.value);
    }else if(ast.type == 'Variable'){
        result = ast.name;
    }else if(ast.type == 'AssignmentExpression'){
        result = ast.left.name + '.assign(' + compile(ast.right)+');';
    }else if(ast.type == 'FunctionCall'){
        result = compile(ast.name) +'('+ ast.arguments.map(function(a){return compile(a)}).join(',') + ')';
    }else if(ast.type == 'PropertyAccess'){
        result = compile(ast.base) + '.' + ast.name;
    }else if(ast.type == 'Function'){
        result = 'function(' + ast.params.join(',') + '){'+
        ast.elements.map(function(a){return compile(a)}).join(';')
        +'}';
    }else if(ast.type == 'ReturnStatement'){
        result = 'return ' + compile(ast.value);
    }else{
        ok = false;
        console.log(ast)
        result = ast;
    }
    //
    return result;
};

これで完結するほどJavaScriptの構文は簡単ではないので、本当はもっと沢山定義があります。
いきなり全ての構文を相手にすると心と骨が折れるので、今回は徐々に突っ込むコードの難易度を上げていきながら、「未定義のものが出てきたら内容をコンソールに出力」を繰り返しました。

ちなみに、今回はコンパイラですが、インタプリタの実装の場合はenvのような変数を突っ込んで、読みながら逐次実行していく感じになると思います。その方が文法を熟知している必要がある分ハードルが高そうです。

ビルトイン関数など

リアクティブな動きを実現するための仕掛けです。

(function(){
    var id = 0;
    var createId = function(){
        return ''+id++;
    };
    
    _Node = function(init){
        this.memo = init;
        this.o = {};
        this.id = createId();
    };
    _Node.prototype.assign = function(in1, in2, f){
        if(this.in1){
            delete this.in1.o[this.id];
        }
        if(this.in2){
            delete this.in2.o[this.id];
        }
        this.in1 = in1;
        this.in2 = in2;
        in1.o[this.id] = this;
        if(in2){
            in2.o[this.id] = this;
            this.calc = function(in1, in2){
                return f(in1.memo, in2.memo);
            }
        }else{
            this.calc = function(in1, in2){
                return in1.memo;
            }
        }
        this.recalc();
        return this;
    };
    _Node.prototype.recalc = function(){
        this.memo = this.calc(this.in1, this.in2);
        this.view();
        this.request();
    };
    _Node.prototype.log = function(){
        console.log(this.memo);
    };
    _Node.prototype.view = function(){};
    _Node.prototype.request = function(){
        for(var k in this.o){
            if(this.o.hasOwnProperty(k)){
                this.o[k].recalc();
            }
        }
    };
})();

全然綺麗じゃないですね…。
これをコンパイル後のソースの頭にくっつけます。

実行風景

それっぽいものが出来たのでコンパイルしてみます。

  • コンパイル前
var a = 1;
var b = 1;
var c = a + b;

("c = " + c).log();// 2

b = 2;

("c = " + c).log();// 3
  • コンパイル後(ビルトイン省略)
var a=new _Node(1)
var b=new _Node(1)
var c=new _Node().assign(a,b,function(a,b){return a+b;})
new _Node().assign(new _Node("c = "),c,function(a,b){return a+b;}).log()
b.assign(new _Node(2));
new _Node().assign(new _Node("c = "),c,function(a,b){return a+b;}).log()
  • 実行結果
2
3

はい、無事に変数bヘの変更が変数cに伝わりました。

おまけ

スプレッドシートっぽいものも作ってみました。
http://jsfiddle.net/jinjor/89mKk/

まとめ

Alt.jsを作ること自体は意外とハードル高くなかったなと思いました。
みんなでオレ様言語作ってカオスをエンジョイしましょう。