JavaScript で Scheme 作った

簡易 SchemeJavaScript で実装した。基本的な関数の呼び出しや定義が行える。
完全に再現したわけでは無いので、Scheme 方言言語 Tetorang と呼ぶことにしよう。

使用はこちら。
http://512bit.org/scheme/scheme.html
Google Chrome で動きます。ぎりぎり FireFox でも動くかも。IE では多分動きません。

使い方

Scheme っぽい言語の REPL。
下部のテキストエリアに scheme 文を打つと、その文と実行結果が上に表示される。実行方法はテキストエリア上で Shift+Enter.
「Shift + Enter で実行」って文の右にグレーで数値が表示されていて、これは括弧の深さを表す。(テキストエリア上で (((()))) とか打つと動作がわかると思う。) この機能は括弧が多い scheme で、括弧の個数がわかりにくなるのでつけた機能。

ではこの REPL で使える書き方の紹介。

シンボル定義

define を使う。

(define x 10)

こんな風に定義して、REPL に x と打ち込めばちゃんと 10 が返される。

関数定義もOK.

// フィボナッチ数を求める関数 fib の定義
(define (fib x)
	(cond ((eq 0 x) 0)
    	((eq 1 x) 1)
        (#t 
        	(+ 
            	(fib (- x 1)) 
                (fib (- x 2))))))

関数呼び出し

上で定義した関数 fib の呼び出し。

> (fib 8)
34

car や cdr の基本関数も使える。使用可能な関数は

(functions) 

で出てくる。

同様に、関数でない定義、つまり単なる値の定義は

(values)

で出せる。

特殊形式の利用

使える特殊形式

  1. define
  2. if
  3. cond
  4. lambda
  5. apply
  6. let
  7. quote

quote は省略記法'(クオート)が使える。

> 'string
quote

lambda はこんな感じ。

> ((lambda (x) (* x x)) 3)
9

apply.

> (apply + '(1.1 2.2 3.3))
6.6

可変長引数

ドット記法による可変長引数のサポート。

(define (add . x)
	( accumulate + 0 x ))

上のように add を定義するとこんな風に引数の数に制限なしで使える。

> (add 1 2 3 4 5)
15

できないこと

マクロの定義、使用はできない。宿題。

実装詳細

さて今回作った言語 Tetorang の実装方法を紹介する。といってもそれほど複雑なことはしておらず、Scheme で表されるリストを JavaScript の配列に変換して順当に処理、という感じ。

(define (add x y)
    (+ x y))

こんなコードがあったとする。これを、JavaScript 上ではこんな風に扱う。

["define",["add","x","y"],["+","x","y"]]

このパース処理で 100 行くらい。あまり適切な表現ではないが、コードとの対比という意味でこの JavaScript 配列に変換されたコードを Tetorang バイナリと呼ぶことにしよう。

このパース処理付近はどうでもいいとして、JavaScript の配列に変換された Tetorang コードを実行するには execute 関数に投げればよい。execute 関数が Tetorang 実装の肝で、これが全てと言っても良い。

第一引数は Tetorang バイナリ。第二引数はシンボルテーブル。Tetorang バイナリを実行中、シンボルの解決をする時に第二引数からシンボルを探す。つまり、名前空間となっている。

execute = function( tree, symbols ){
    if( symbols == undefined )
        symbols = new Symbols();

    depth++;
    if( depth > 10000 )
        throw("maybe infinity loop. aborted.");

	DEBUG && console.log(printResult( tree ));

    // nil
    if( isNil( tree ) )
        return 'nil';
        
    // string
    if( isString( tree ) )
        return tree.substring(1, tree.length - 1);

    // value
    if( isValue( tree ) )
        return tree;
        
    // symbol
    if( isSymbolName( tree ) )
        return findSymbol( tree, symbols );
        
    // special form
    if( isSpecial( tree ) )
        return Special[ tree[0] ]( tree, symbols );
    
    // builtin function
    if( isBuiltinFunc( tree, symbols ) ) {
        var lambda = execute( tree[0], symbols );
        var args = tree.slice(1).map( function(v){ return execute(v,symbols); });
        return lambda.apply( symbols, args );
    }

    // user defined function            
    if( isUserDefinedFunc( tree, symbols ) ){
        var lambda = execute( tree[0], symbols );
        var newSymbols = bindArguments( lambda[0], tree, symbols );
        return execute( lambda[1], newSymbols );
    }
    
    throw( "unknown object : " + tree );
};

isList や isValue 等は容易に想像がつくと思うので省略する。ちょっと複雑な関数呼び出しは、ユーザー定義関数を呼び出す際に呼び出される bindArguments 関数だろう。これは、

(define (f x) (car x))

のような関数を呼び出す際に仮引数 x に実引数を束縛する関数だ。

シンボルテーブルの実態である Symbols オブジェクトを new して新たなテーブルを作成する。そしてその新たなシンボルテーブル、newSymbols に対して、仮引数と実引数のペアをシンボルテーブルに登録する。(registerSymbol 関数)

bindArguments = function( parameter, tree, symbols ){
    var newSymbols = new Symbols();
    newSymbols.prototype = symbols;
    for( var i=0; i<parameter.length; i++ ) {
        if( parameter[i] == '.' ) {
            // 可変長引数 これ以降をリストにまとめて渡す
            var ends = [];
            for( i++; i < tree.length; i++ ){
                var ret = execute( tree[i], symbols );
                ret = isNil(ret) ? 'nil' : ret;
                ends.push(ret);
            }
            registerSymbol( parameter[parameter.length-1], ends, newSymbols );
            break;
        }
        var ret = execute( tree[i+1], symbols );
        ret = isNil(ret) ? 'nil' : ret;
        registerSymbol( parameter[i], ret, newSymbols );
    }
    return newSymbols;
};

可変長引数は少々汚いコードだが、やっていることは仮引数中に . (ドット) が現れたらそれ以降の実引数をリストにして束縛しているというコードだ。

次に、基本関数や nil, #t 等をシンボルテーブルテーブルに登録する。

registerSymbol( 'nil', 'nil' );
registerSymbol( '#t', '#t' );
registerSymbol('car', function(list){
    if( arguments.length != 1 )
        throw("'car' requires 1 arguments.");

    if( isNil(list) )
        return 'nil';

    if( !(list instanceof Array) )
        throw("car invalid argument : "+list);

    return list[0];
});

この調子で基本5関数( car, cdr, atom, eq, cons )を定義して、+ や - 等の基本演算子も定義すればチューリング完全な言語の完成なり。

感想

Scheme の処理系を JavaScript で作ろう」と思い立った時、Scheme のコードは一行たりとも書いたことなかったのだけど、なんとかなるものだ。
あとはマクロと、ガベージコレクタも作りたい。

JavaScript で数値判定

与えられた x が数値か、もしくは文字列での数字かどうかを調べる。数値または数字なら true.

function isNumber(x){ 
    if( typeof(x) != 'number' && typeof(x) != 'string' )
        return false;
    else 
        return (x == parseFloat(x) && isFinite(x));
}


テスト

isNumber(-1) // true
isNumber(0) // true
isNumber(1) // true
isNumber(0.0) // true
isNumber(1.5) // true
isNumber(10e+3) // true
isNumber(10e-3) // true
isNumber("-1") //true
isNumber("0")  // true
isNumber("1") //true
isNumber("0.0") // true
isNumber("1.5") // true
isNumber("10e+3") // true
isNumber("10e-3") // true

isNumber(true) //false
isNumber(false) //false
isNumber(null) //false
isNumber(undefined) //false
isNumber([]) //false
isNumber({}) //false
isNumber([1]) //false
isNumber(NaN) //false

前にも似たエントリを書いたけどこっちのほうがシンプルなので。

パーフェクトJavaScript

パーフェクトJavaScript

How to 消されない広告の作り方

数年ぶりに Proxomitron を導入して、相変わらず素敵なツールだったので今後の Proxomitroner のために現代の Web ページでどのように広告が表示されているかについて記す。

最も単純な広告

a タグで表示。

<a href="http://sample-ad.com/">絶対儲かる株100選</a>

進化系 v1

単純な a タグで広告をベタ貼りしたのでは単純なフィルタで消されてしまう。
よって、JavaScript で広告を出力させる方法を取る。

<script type="text/javascript">
docment.write('<a href="http://sample-ad.com/">絶対儲かる株100選</a>');
</script>

進化系 v2

v1 もまた簡単なテキストマッチングによって消されてしまうので、外部スクリプトに分ける。

sample-ad.js

docment.write('<a href="http://sample-ad.com/">絶対儲かる株100選</a>');

webページ

<script type="text/javascript" src="sample-ad.js"></script>

進化系 v3

v2 に対して広告キラーやフィルタは「<script type="text/javascript" src="sample-ad.js"></script> を無効にする」という処理を行う。
これをかいくぐるため、クライアントがダウンロードした時点のソースでは <script type=〜〜 </script> を記載せず、スクリプトを動的にロードするためのスクリプトを書く。

sample-ad.js

docment.write('<a href="http://sample-ad.com/">絶対儲かる株100選</a>');

webページ

<script type="text/javascript">
    document.write('<script type="text/javascript" src="sample-ad.js"></script>');
</script>

進化系 v4

v3 の時点である程度の広告キラーをかいくぐってしまうが、まだ引っかかる場合がある。
document.write で script タグを出力するのは広告くらいしかありえないので、
「script タグを含むタグを出力するスクリプトを全て無効にする」というフィルタに引っかかる場合だ。

これを回避するため、以下のようにして単純な正規表現には引っかからないよう誤魔化す。

<script type="text/javascript">
    document.write('<scr' + 'ipt type="text/javascript" src="sample-ad.js">'+'</scr' + 'ipt>');
</script>

これで一見スクリプト中に document.write で script タグを出力するようには見えなくなり、正規表現にマッチせず広告が出力される。

進化系 v4 亜種

unescape することもしばしば。

<script type="text/javascript">
    document.write(unescape("%3Cscript") + ' type="text/javascript" src="sample-ad.js">'+'</scr' + 'ipt>');
</script>

進化系 v4 亜種2

write がダメなら create.

<script type="text/javascript">
    var s = document.createElement('scr'+'ipt');
    s.src = "sample-ad.js";
    document.body.appendChild( s );
</script>

進化系 v5

現代の広告はたいてい v4, v4 亜種程度で収まっているが、質の悪い v5 が存在する。
それは、あえて script タグの文法を間違って記述することでフィルタを回避し、かつブラウザの解釈で文法ミスを自動的に補完する場合。
例えば以下のコードは script の閉じタグが完全に閉じておらず、文法ミスだ。

<script type="text/javascript" src="sample-ad.js"></script

しかし通常の広告フィルタは のような閉じタグがあることを前提としているので、文法ミスに対しては無力である。

進化系 v6

上記 v2 〜 v5 まで、広告用の JavaScript ファイルを生成して読み込ませていた。
Adblock のような JavaScript レベルでの広告カットではこれで十分だが、Proxomitron は条件にマッチしたリソースへのコネクションを切ることが出来る。つまり、sample-ad.js というスクリプトをダウンロードしないというフィルタを書けば上記のものは全て対応できることになる。
(ただしこのコネクションフィルタは特定のスクリプトを弾くような使い方は非効率なのであまり行わない。ファイル名を変えるだけで太刀打ちできなくなる。)

よって、広告用の JavaScript はフィルタされる可能性があるのでここで奥義を使う。
それは、そのサイトのコンテンツの JavaScript と同じファイルにすることである。
例えば、jQuery.js 内に広告用の JavaScript を書いたとして、jQuery.js に対するコネクションをフィルタするわけにはいかない。ページの閲覧どころではなくなるからだ。


理論的には JavaScript コード内の広告出力に関する部分のみカットできれば良いわけだが、フィルタ界のキング Proxomitron でもそのようなフィルタは一般的ではない。誤爆の可能性が大きいからだ。

しかしながら現実にはこの手法の広告はあまり一般的ではない。サイトオーナーが広告を入れる手間がかかってしまうためだ。v2 のように script タグ一行入れるのならサイトオーナーも楽で、広告主としても案内が楽である。v6 のような苦労をして広告を表示したところで、それは通常に比べて高度な広告フィルタを入れているユーザー分のみ多く表示できるというメリットしかない。それはメリットがあまりに少ないので、現実的には広まっておらず、たいていは v4 までである。


まとめ

なぜか document.write( '

[Patterns]
Name = "kill scr ipt ad"
Active = TRUE
Bounds = "<script\s*</script>"
Limit = 2048
Match = "*("
        "('<scr' \+ 'ipt)"
        "|("<scr" \+ "ipt)"
        "|(createElement\('script'\))"
        "|(write\('<scr'\))"
        "|unescape("%3Cscript")"
        ")*"
Replace = "<!-- scr ipt ad killed -->"

Proxomitron の良いところは、フィルタしてしまえばスクリプトをダウンロードしたり実行する処理が減るので、ブラウジングが軽くなること。特に、広告が非常に多いエロサイトは自分向けにカスタマイズすれば超快適でサクサク、ムラムラなブラウジングを行うことができる。

数値判定する関数

JavaScript で数値判定を行う関数。isNaNは使い物にならないのでこのような関数を自前で用意する。
Array のインスタンスを弾いているのは、[1] のような引数を防ぐため。[1]を String にキャストすると "1" となり、数値とみなされてしまうのだ。

function isNumber(value)
{
    if(value instanceof Array)
        return false;
    
    //trim
    value = String(value).replace(/^[  ]+|[  ]+$/g, '');

    if(value.length == 0)
        return false;
        
    if(isNaN(value) || !isFinite(value))
        return false;
    
    return true;
}


テスト。

isNumber(1); //true
isNumber(0); //true
isNumber(-1); //true
isNumber("1"); //true
isNumber("0"); //true
isNumber("-1"); //true
isNumber(1.1); //true
isNumber(.1); //true
isNumber("1.1"); //true
isNumber(".1"); //true
isNumber(10e-3); //true
isNumber(10e+3); //true
isNumber("10e-5"); //true
isNumber("10e+5"); //true

isNumber("10f-5"); //false
isNumber("10f+5"); //false
isNumber("1 1"); //false
isNumber(" 1 1 "); //false
isNumber(null); //false
isNumber(true); //false
isNumber(false); //false
isNumber(undefined); //false
isNumber("a"); //false
isNumber("1a"); //false
isNumber(""); //false
isNumber(" "); //false
isNumber([]); //false
isNumber([1]); //false
isNumber([1,1]); //false
isNumber({}); //false
isNumber({a:1}); //false
isNumber({a:1, b:2}); //false
isNumber(new Object()); //false
isNumber(); //false