トップ «前の日記(2008-05-27) 最新 次の日記(2008-05-30)» 編集

Route 477



2008-05-29

[biwascheme] JavaScriptでS式を記述するいい方法はないものか

方法1: new Pairのベタ書き。

 new Pair(1,
   new Pair(2,
     new Pair(3, nil)));

このように、リストの各要素ごとにインデントし、最後の行にnilを書くのが読みやすいです。

方法2: Array#to_list。

 [1, 2, 3].to_list();

ベタ書きはさすがに面倒なので、最近はこんな感じで書けるようにしていました。 でも、リストがネストしたときに読みにくいのがちょっと困る。

 //例: (let ((x 1)) y)
 [Sym("let"), [[Sym("x"), 1].to_list()].to_list(), Sym("y")].to_list();

これを解決するために考えたのが:

方法3: BiwaScheme.List。

 //例: (let ((x 1)) y)
 List(Sym("let"), List(List(Sym("x"), 1)), Sym("y"));

どうでしょうか?

「もっとこう書けるよ」というのがありましたらぜひ教えてください m(__)m

[biwascheme] condとcaseを実装しました

最近関西で始まったGauche本読書会やSICP読書会でBiwaSchemeを試していただいたのが、 そこで困ったのが「condとcaseがない」こと。

っていうかSICPってわりと初めの方からcondとか出てくるんだなー(一章はifとかだけだろと思い込んでいた^^;)というのは置いといて、 せっかく使ってもらえるのにcondが無いのは悲しいねってことで実装してみました。

[biwascheme] cond

以下、これからScheme処理系を実装する人のためにメモを残しておきます。

condはRubyでいう「caseの引数がないやつ」というか(←わかりにくいな)、 if〜elseを順に並べたようなものです。

 (define m 12)
 (print (cond
          ((<= 3 m 5)  "spring")
          ((<= 6 m 8)  "summer")
          ((<= 9 m 11) "autumn")
          (else        "winter")))

で、R6RSの仕様書によるとそれぞれの節(clause)は以下のような形をしているようです。

まずは条件とそれが成り立ったときの処理がかかれてい普通の節。 これはifに展開すればOKでしょう。

 // pattern 1: (test expr)
 //  -> (if test expr ret)

次に、exprがなくて条件だけの場合。この場合は、条件式の結果が返り値になるそうです。 じゃあorで。

 // pattern 2: (test)
 //  -> (or test ret)

さらに、「=>」を使うことで条件式の結果を関数に渡すこともできます。 これってR6RSからかと思ってたけど、 んですね。

この場合は、条件式の結果を保存するための一時変数が必要になります。 ここで問題になるのが一時変数の名前で、下手なものを選ぶとユーザが使っている変数名と バッティングして大変なことに…。

BiwaSchemeではとりあえず、__gensym_1 のような「被らなさそうなシンボル」を 生成することにしました。本当は、ユーザが '__gensym_1 のようなシンボルを使っても 大丈夫なように実装しないといけないんですが、まずは「そういうシンボルは使わないで ください」という方針で行きましょう。

 // pattern 3: (test => expr)
 //  -> (let ((#<gensym1> test))
 //       (if test (expr #<gensym1>) ret))

さて、condの最後の節にはelse節を置くことができます。 これもいくつかのパターンに分けられます。

まずは普通のelse節。基本的にはbeginを使えばOKですが、展開後の式をコンパクトにするため、 式が一つだけだった場合はbeginを省略することにしましょう。*1

 // pattern A: (else expr ...)
 //  -> (begin expr ...)
 // pattern B: (else expr)
 //  -> expr

次に、exprが省略されたelse節。このとき何を返すかはR5/6RSでは未定義っぽいんですが、 Gaucheが#fを返してたので#fを返すことにしました。

 // pattern A: (else)
 //  -> #f            ; not specified in R6RS...?

最後に、else節がない場合。つまりどの節の条件も成り立たなかったとき、 R6RSでは「unspecified」な値を返すことになっています。

 // pattern D: no else clause
 //  -> #<undef>

と、ここまで書いてしまえば、実装するのは比較的簡単でした。このエントリの最後に貼っておきます。

実装

  define_syntax("cond", function(x){
    var clauses = x.cdr;
    if(!(clauses instanceof Pair) || clauses === nil){
      throw new Error("malformed cond: cond needs list but got " +
                      to_write_ss(clauses));
    }
    // TODO: assert that clauses is a proper list

    var ret = null;
    clauses.to_array().reverse().each(function(clause){
      if(!(clause instanceof Pair)){
        throw new Error("bad clause in cond: " + to_write_ss(clause));
      }

      if(clause.car === Sym("else")){
        if(ret !== null){
          throw new Error("'else' clause of cond followed by more clauses: " +
                          to_write_ss(clauses));
        }
        else if(clause.cdr === nil){
          // pattern A: (else)
          //  -> #f            ; not specified in R6RS...?
          ret = false;
        }
        else if(clause.cdr.cdr === nil){
          // pattern B: (else expr)
          //  -> expr
          ret = clause.cdr.car;
        }
        else{
          // pattern C: (else expr ...)
          //  -> (begin expr ...)
          ret = new Pair(Sym("begin"), clause.cdr);
        }
      }
      else if(ret === null){
        // pattern D: no else clause
        //  -> #<undef>
        ret = undefined;
      }
      else{
        var test = clause.car;
        if(clause.cdr === nil){
          // pattern 1: (test)
          //  -> (or test ret)
          ret = [Sym("or"), test, ret].to_list();
        }
        else if (clause.cdr.cdr === nil){
          // pattern 2: (test expr)
          //  -> (if test expr ret)
          ret = [Sym("if"), test, clause.cdr.car, ret].to_list();
        }
        else if(clause.cdr.car === Sym("=>")){
          // pattern 3: (test => expr)
          //  -> (let ((#<gensym1> test))
          //       (if test (expr #<gensym1>) ret))
          var test = clause.car, expr = clause.cdr.cdr.car;
          var tmp_sym = BiwaScheme.gensym();

          ret = List(Sym("let"),
                     List( List(tmp_sym, test) ),
                     List(Sym("if"), test, List(expr, tmp_sym), ret));
        }
        else{
          // pattern 4: (test expr ...)
          //  -> (if test (begin expr ...) ret)
          ret = [Sym("if"), test,
                   new Pair(Sym("begin"), clause.cdr),
                   ret].to_list();
        }
      }
    });
    return ret;
  });

*1 これは「なんとなく無駄が嫌」という理由によるもので、この最適化にどのくらい効果があるのかは不明です^^;; 良い子のみんなはちゃんとボトルネックを測ってから最適化しようね!

[biwascheme] case

caseも、condと考え方はおなじ。

まず、keyを一時変数に代入しておきます。

   // (case key clauses ....)
   //  -> (let ((#<gensym1> key))

で、普通の節だったら、eqv?に展開。

   // pattern A: ((datum ...) expr ...)
   //  -> (if (or (eqv? key (quote d1)) ...) (begin expr ...) ret)

elseだったら単純にbeginに展開。

   // pattern B: (else expr ...)
   //  -> (begin expr ...)

[biwascheme] macroexpand族

caseのデバッグで欲しくなったので、macroexpand一族も実装してみた。

最初は何をしたらいいのかが分かってなくて困ったけど、できてみれば簡単だった。

  • %macroexpandはマクロで、与えられた式をマクロでなくなるまで展開する。これを行う関数はもうある(Interpreter#expand)ので簡単。
  • %macroexpand-1はマクロを1段だけ展開する。要するにTopEnvからマクロ変換器を取ってきて、一回だけ実行してやればいい。
  • %がついてない方 (macroexpand, macroexpand-1) は、単にマクロから関数になっただけ(quoteされたSchemeプログラムを受け取る)。
  var macroexpand_1 = function(x){
    if(x instanceof Pair){
      if(x.car instanceof Symbol && TopEnv[x.car.name] instanceof Syntax){
        var transformer = TopEnv[x.car.name];
        x = transformer.transform(x);
      }
      else
        throw new Error("macroexpand-1: `" + to_write_ss(x) + "' is not a macro");
    }
    return x;
  }
  define_syntax("%macroexpand", function(x){
    var expanded = (new Interpreter).expand(x.cdr.car);
    return [Sym("quote"), expanded].to_list();
  });
  define_syntax("%macroexpand-1", function(x){
    var expanded = macroexpand_1(x.cdr.car);
    return [Sym("quote"), expanded].to_list();
  });

  define_libfunc("macroexpand", 1, 1, function(ar){
    return (new Interpreter).expand(ar[0]);
  });
  define_libfunc("macroexpand-1", 1, 1, function(ar){
    return macroexpand_1(ar[0]);
  }); 

[biwascheme][idea] with-output-to-elementとかで

printの出力結果がぜんぶdiv要素に入るとか良くね?

 ;; <div id="foo"></div>

(with-output-to-element ($ "foo")
   (print "hello")
   (print "world"))

(get-content ($ "foo"))  ;; → "hello\nworld\n"