Archive for 12月, 2014

[LSP42] HaxeとF#

月曜日, 12月 15th, 2014

(この記事はLISP Implementation Advent Calendar 15日目のためのエントリです。)

HaxeF#でLISPを作りました。
https://github.com/zick/HaxeLisp
https://github.com/zick/FSharpLisp

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその25〜26個目です。
両方とも名前を思いついたから選んだといった感じです。HaxeはNekoを使った時に、NekoVM上で動く言語として紹介されているのを見て知りました。F#は前回C#をやったから選んだといった感じです。

Haxeの思い出

代数的データ型

Haxeには代数的データ型があります。私は特に代数的データ型が好きなわけではないんですが、少し前にSMLとOCamlを使って少し慣れたので、使ってみることにしました。

enum LObj {
  Nil;
  Num(num : Int);
  Sym(name : String);
  Error(msg : String);
  Cons(cell : Cell);
  Subr(fn : LObj -> LObj);
  Expr(args : LObj, body : LObj, env : LObj);
}

パターンマッチはswitchで行います。

public static function print(obj) {
  return switch (obj) {
    case Nil: "nil";
    case Num(num): Std.string(num);
    case Sym(name): name;
    case Error(msg): "";
    case Cons(cell): printList(obj);
    case Subr(fn): "";
    case Expr(args, body, env): "";
    default: "";
  }
}

ただ、このswitch、各節にcaseと書かなければならないので、タイプ数がわりと多くなるのがあまり好きじゃありません。

型推論

Haxeは型を書かなくてもコンパイラが型推論を行ってくれるのですが、たまに型推論に失敗してエラーが出ます。

private static function skipSpaces(str: String) {
  var i = 0;
  while (i < str.length) {
    if (!isSpace(str.charAt(i))) {
      break;
    }
    i++;
  }
  return str.substring(i);
}

この skipSpaces の引数 str には型を明記しないとコンパイルエラーが出ます。

HaxeLisp.hx:169: characters 21-24 : String should be { substring : Int -> Unknown<0>, length : Int, charAt : Int -> String }
HaxeLisp.hx:169: characters 21-24 : Invalid type for field substring :
HaxeLisp.hx:169: characters 21-24 : startIndex : Int -> ?endIndex : Int -> String should be Int -> Unknown<0>
HaxeLisp.hx:169: characters 21-24 : For function argument 'str'
HaxeLisp.hx:188: characters 23-26 : String should be { substring : Int -> Unknown<0>, length : Int, charAt : Int -> String }
HaxeLisp.hx:188: characters 23-26 : Invalid type for field substring :
HaxeLisp.hx:188: characters 23-26 : startIndex : Int -> ?endIndex : Int -> String should be Int -> Unknown<0>
HaxeLisp.hx:188: characters 23-26 : For function argument 'str'

エラーメッセージを眺めた限り「strはlengthとcharAtとsubstringというメソッドを持つ」というところまでは推論できるものの「substringの型がわからない」という理由でエラーになっているように見えます。でもまあ、なぜ型推論に失敗するかを考えるより、型を書いた方が早いという最低な理由で型を書くことにしました。

F#の思い出

F#はOCamlによく似ていて、慣れている人がやれば10行もいじればOCamlで書いたLISPをF#に移植できるらしいのですが、私はF#もOCamlも慣れていないので一から書き直すことになりました。

アドレス比較

OCamlには中身の比較を行う = と アドレスの比較を行う == という2種類の演算子があります。F#にも同様のものがあるので使おうとしたら、なんと、 == が代数的データ型に使えないという悲しい事実が分かりました(*)
F#にはクラスもあるので、LISPのオブジェクトはクラスを使って表現することにしました。

実際には1行追加したら使えるようになったみたいだよ、お兄ちゃん!

クラス

という訳でこんなクラスを定義しました。

type LObj(tag : String) =
  member this.tag = tag
type Nil() =
  inherit LObj("nil")
type Num(n : Int32) =
  inherit LObj("num")
  member this.num = n
type Sym(s : String) =
  inherit LObj("sym")
  member this.str = s
type Error(s : String) =
  inherit LObj("error")
  member this.str = s
type Cons(a : LObj, d : LObj) =
  inherit LObj("cons")
  member val car = a with get, set
  member val cdr = d with get, set
type Subr(fn : LObj -> LObj) =
  inherit LObj("subr")
  member this.fn = fn
type Expr(args : LObj, body : LObj, env : LObj) =
  inherit LObj("expr")
  member this.args = args
  member this.body = body
  member this.env = env

なんか tag とかいうものが出てきますが、実際には使っていません。メンバのないクラスの書き方がよくわからなかったので取り敢えず書いただけです。それを除くと(私の目には)それなりにまっとうに見えます。しかし、ここからどんどん雲行きが怪しくなっていきます。

let o obj = obj :> LObj

let makeNum n = o(Num n)
let makeError s = o(Error s)
let makeCons a d = o(Cons (a, d))
let makeSubr fn = o(Subr fn)
let makeExpr args env = o(Expr (safeCar args, safeCdr args, env))

この :> という演算子はキャストです。サブクラスのまま扱われたら困るので、何をするにもベースクラスにキャストしてやらないといけないわけです。

let rec nreconc (lst : LObj) (tail : LObj) =
  match lst with
  | :? Cons as c ->
      let tmp = c.cdr in
        c.cdr <- tail;
        nreconc tmp lst
  | _ -> tail

さて、このnreconcの定義、何か変だと思いませんか? そう。引数の型を明記しています。どうやらクラスを使うと型をうまく推論してくれないみたいです。もはやOCamlに似ている言語を使っているのだかよく分からなくなってきました。

オフサイドルール

F#の良かった点はインデントでブロックを表現するオフサイドルールを使っていることでした。個人的にはオフサイドルールはそれほど好きではないのですが、ML系言語の微妙なところ(入れ子の式には括弧を付けないといけない、まれにセミコロンを書かないといけない)といったところを避けれるのはわりとありだと思いました。

小学生並みの感想

型推論があるから型を書かなくてもいいと期待している時に型を書かされるとショックが大きかったです。

[LSP42] DとJavaとC#

日曜日, 12月 14th, 2014

(この記事はLISP Implementation Advent Calendar 14日目のためのエントリです。)

DJavaC#でLISPを作りました。
https://github.com/zick/DLisp
https://github.com/zick/JavaLisp
https://github.com/zick/CSharpLisp

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその22〜24個目です。
JavaとC#は明らかにメジャーな言語ですし、Dは少なくても名前は知っている人は多いでしょう。こういった言語を使ってもあまり喜ぶ人も少ないのですが、42個のうち半分を超えたのでいまのうちにこっそり使っておこうという発想で選びました。42個目の言語がJavaだったりするとあまりにも残念な感じになってしまいますから。

Dの思い出

Dはまったく見たことも書いたこともなかったのですが、Cを知ってたらほとんど新しいことを勉強しなくても書けてしまいました。コンパイラのエラーメッセージも読みやすいので楽ちんです。

外観

だいたいCみたいな感じです。

LObj nreverse(LObj lst) {
  LObj ret = kNil;
  while (lst.tag == Type.Cons) {
    LObj tmp = lst.data.cons.cdr;
    lst.data.cons.cdr = ret;
    ret = lst;
    lst = tmp;
  }
  return ret;
}

便利な機能を使う時以外はCと思って書けば書けます。私にとっては非常に楽でした。

データ構造

例によってクラスを1つだけ作るというインチキを使いました。

enum Type {
  Nil, Num, Sym, Error, Cons, Subr, Expr
}

class LObj {
  struct Cons {
    LObj car;
    LObj cdr;
  }
  ...
  union Data {
    int num;
    string str;
    Cons cons;
    Expr expr;
    Subr subr;
  }

  this(Type type) {
    tag = type;
  }
  ...
  this(Type type, LObj a, LObj d) {
    tag = type;
    data.cons.car = a;
    data.cons.cdr = d;
  }
  ...

  Type tag;
  Data data;
}

今回はtagにenumを使っているので、少なくても文字列よりは間違いが起きないでしょう。Fantomのような名前付きコンストラクタはないので、コンストラクタの第一引数にはtagを付けるようにしました。あとは特筆すべき点はないかと思います。

SUBR

まるでCの関数ポインタのようなことができるのでSUBRも楽に作れます。

LObj subrCons(LObj args) {
  return makeCons(safeCar(args), safeCar(safeCdr(args)));
}
...
addToEnv(makeSym("cons"), new LObj(Type.Subr, &subrCons), g_env);

ただ、「クロージャのポインタを取得するには delegate を使って云々……」みたいなことがマニュアルに書いてあったのですが、面倒そうなのでクロージャは使わないという方向で逃げました。

Javaの思い出

Javaは一応書いたことはあるのですが、Javaアプレットやiアプリ、それをMIDP用に書き直したもの、などの簡単なゲームをちょっと書いたことがある程度です。特にiアプリではメモリや速度の関係で「staticおじさん」にならざるを得ない感じでしたし、それどころか else を削ることでバイトコードを節約するなど、通常のJavaとは違う何かを書いただけのような気もします。あと、大学生に「iアプリって何ですか」と先日言われたのがなんだかショックで仕方ありません。

外観

Dと比べると冗長な感じがします。

public static LObj nreverse(LObj lst) {
    LObj ret = kNil;
    while (lst.tag() == Type.CONS) {
        LObj tmp = lst.cons().cdr;
        lst.cons().cdr = ret;
        ret = lst;
        lst = tmp;
    }
    return ret;
}

さっそくstaticおじさんになっております。

データ構造

クラスを1つしか作らないのはDのときと同じですが、Javaには共用体がありません。

enum Type {
    NIL, NUM, SYM, ERROR, CONS, SUBR, EXPR,
}

class LObj {
    public Type tag() { return tag_; }
    public LObj(Type type, Object obj) {
        tag_ = type;
        data_ = obj;
    }
    public Integer num() {
        return (Integer)data_;
    }
    ...
    public Cons cons() {
        return (Cons)data_;
    }

    ...

    private Type tag_;
    private Object data_;
}

class Cons {
    public Cons(LObj a, LObj d) {
        car = a;
        cdr = d;
    }
    public LObj car;
    public LObj cdr;
}

しかし、 Object というすべてのベースとなるクラスがあるので、キャストするだけです。型安全とか知りません。

SUBR

ちらっと人のコードを見たところ、Javaはインスタンスを作るときにクラスの一部分をオーバライドできるらしいじゃないですか。気になったのでこれを使ってSUBRを作りました。

Subr subrCons = new Subr() {
        @Override public LObj call(LObj args) {
            return Util.makeCons(Util.safeCar(args),
                                 Util.safeCar(Util.safeCdr(args)));
        }
};

実際にはinner-classが作られているんでしょうか。内部的なことは分かりません。
あと、最近のJavaにはクロージャがあるとかないとかいう話は知りません。

C#の思い出

C#は書いたことがなかったのですが、それよりもいくら探してもC#のロゴらしきものが見当たらなかった方が問題です。だれかC#のロゴが何なのか教えてください。

外観

Javaと同程度の冗長さでしょうか。

public static LObj nreverse(LObj lst) {
  LObj ret = kNil;
  while (lst.tag() == Type.Cons) {
    LObj tmp = lst.cons().cdr;
    lst.cons().cdr = ret;
    ret = lst;
    lst = tmp;
  }
  return ret;
}

といより、ほぼJavaと同じに見えます。

データ構造

Javaのときと完全に同じ戦略です。データはすべて object というすべてのベースとなるクラスにキャストします。型安全とか知りません。

enum Type { Nil, Num, Sym, Error, Cons, Subr, Expr };

class LObj {
  public Type tag() { return tag_; }

  public LObj(Type type, object obj) {
    tag_ = type;
    data_ = obj;
  }

  public Int32 num() {
    return (Int32)data_;
  }
  ...
  public Cons cons() {
    return (Cons)data_;
  }
  ...

  private Type tag_;
  private object data_;
}

class Cons {
  public Cons(LObj a, LObj d) {
    car = a;
    cdr = d;
  }
  public LObj car;
  public LObj cdr;
}

SUBR

よく分かりませんが、delegateというのを使えばクロージャが作れるらしいです。Dのときに同じ話を聞いた気もします。

delegate LObj Subr(LObj ags);

Subr subrCar = delegate(LObj args) {
  return Util.safeCar(Util.safeCar(args));
};

何をやってるのかよく分かりませんが、こんなんで動きました。

小学生並みの感想

「よく分からないけど動く」というのはある意味非常に大事なことだと思います。

[LSP42] Standard MLとOCaml

土曜日, 12月 13th, 2014

(この記事はLISP Implementation Advent Calendar 13日目のためのエントリです。)

Standard MLOCamlでLISPを作りました。
https://github.com/zick/SMLisp
https://github.com/zick/OCamLisp

Standard MLもOCamlもML系の言語で非常に良く似ています。が、文法はちょっとずつ違っていてCommon LispとSchemeのような薫りがします。

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその20〜21個目です。
私は変数に型が無い言語や、型が緩い言語が好きなんですが、そういった言語を探すのにも疲れてきたし、ついに20言語を突破するということで、思い切って型の強いStandard MLとOCamlを選びました。両方ともほとんど書いたことはありませんでしたが、片方を書けばもう片方を書くのは非常に簡単なはずという打算もありました。

Standard MLの思い出

外観

Standard MLの文法といえば、きっちりとBNFが定義されていることで有名(?)ですが、自分で書いてみるとなかなか面白いことに気付きました。

datatype obj =
    NIL
  | NUM of int
  | SYM of string
  | ERROR of string
  | CONS of ((obj ref) * (obj ref)) ref
  | SUBR of (obj -> obj) ref
  | EXPR of obj * obj * obj

fun safeCar obj =
  case obj of
       CONS(ref(ref a,  ref d)) => a
     | _ => NIL

fun safeCdr obj =
  case obj of
       CONS(ref(ref a,  ref d)) => d
     | _ => NIL

何が面白いかというと、文の区切り目となる記号がないことです。endも無ければ中括弧{}も使いません。上のプログラムだと空行で区切られているように見えますが、実際には続くトークンを見て文が続くか、別の文が始まるか区別しているわけです。文の入れ子がある場合は括弧()を付けて区別します。これはこれできれいな文法だと思うんですが謎なのが let の存在。

fun nreconc lst tail =
  case lst of
       CONS(ref(a, d)) =>
         let val tmp = !d in
           d := tail;
           nreconc tmp lst
         end
     | _ => tail

なぜか let には end が付きます。正直意味が分かりません。ちょっときれいだと思った文法が急に汚く見えてきました。誰かこの end が存在する理由を教えて下さい。

Standard MLのキーワードはfun、val、opなど短いものが多く、私好みなんですが、私はこういった名前を無意識に変数に付けてしまい、コンパイルエラーになるということが多々ありました。それは私が悪いんですが、困ったことにStandard MLのコンパイラ(Standard ML of New Jersey)のエラーメッセージが分かりにくく、必要以上に苦労した覚えがあります。

強い型

ちゃんと代数的データ型を使ってLISPの型を定義しました。本や論文などで散々見てきたんですが、実際に使って100行以上のプログラムを書くのは初めてです。特別簡単だったということもなければ、特別難しかったということもありませんでした。ただ慣れていない言語を使っているため色々と戸惑った、という感じでした。ほとんどのミスはコンパイル時に見つかり、実行時にミスが見つかることはほとんどなかった、というのは型の緩い言語とは違うところでしょうが、LISPの完成までの時間がこれにより縮まったかというとそんなことはありませんでした。慣れのほうが大事ですね。

参照型

Standard MLの変数はすべて immutable で、参照型をもつ値のみ中身を書き換えることが出来ます。今回の場合はコンスセルのcarとcdrに参照型を使えばいいんですが、上のプログラムをよく見てみると、コンスセル全体が参照型になってますし、SUBRも参照型を使っています。これはオブジェクトのアドレス比較の方法が分からず困っていたら「参照型だと間違いなくアドレス比較だろ」という悪知恵が働いてしまったためです。正しいやり方は正しい人に聞いてください。

相互再帰

型を推論する都合上か、相互再帰をする関数は and で繋ぐ必要があります。

fun printObj obj =
  case obj of
       NIL => "nil"
     | NUM n => Int.toString(n)
     | SYM s => s
     | ERROR s => ""
     | CONS _ => "(" ^ (printList obj "" "") ^ ")"
     | SUBR(_) => ""
     | EXPR(_, _, _) => ""
and printList obj delimiter acc =
  case obj of
       CONS(ref(ref a, ref d)) =>
         printList d " " (acc ^ delimiter ^ printObj a)
     | NIL => acc
     | _ => acc ^ " . " ^ printObj obj

2つの関数を and で繋ぐくらいならいいのですが、LISPのevalを愚直に実装すると、複数の関数が相互再帰をするので and が続いてなんだか見苦しい感じになります。これは何とかしたいところ。

短い!

さて、出来上がったソースコードを見てみると、ほぼちょうど300行。かなり短いです。文末の end などがなく、それによる改行がないのが効いているのではないかと思います。ML系の言語は初めて書いたので冗長なコードを書いている可能性が大いにあるため、慣れている人が書いたらより短くなる可能性があります。

OCamlの思い出

「Standard MLのコードの一部を書き換えてやれば動くんじゃないの?」という悪魔のささやきが聞こえてきましたが、聞かなかったことにして一から書きました。

外観

全体的にOCamlの方がStandard MLよりスッキリしているように見えます。

type obj =
  Nil
| Num of int
| Sym of string
| Error of string
| Cons of (obj ref) * obj ref
| Subr of (obj -> obj)
| Expr of obj * obj * obj

let safeCar obj =
  match obj with
    Cons(a, d) -> !a
  | _ -> Nil

let safeCdr obj =
  match obj with
    Cons(a, d) -> !d
  | _ -> Nil

let rec nreconc lst tail =
  match lst with
    Cons(a, d) ->
      let tmp = !d in
        d := tail;
        nreconc tmp lst
  | _ -> tail

let の終りを示す謎の end が消えました。私としてはかなり好感度が高いです。他の細かな違いも嫌いじゃないです。正直なところ、OCamlを自分で書くまでは「MLから派生しておきながら文法をちょっとずつ変えるとはけしからん。ほぼ同じにするかまったく変えるかどっちかにしろ」と思っていたんですが、OCamlを書いてみると、純粋にこっちのほうがいいな、と思いました。ごめんなさい。
Common LispとSchemeを両方書くときに生じる混乱がStandard MLとOCamlでも起きるかとおもいきや、いうほどStandard MLに慣れていなかったので案外平気でした。
あと、Standard MLのときはコンパイルエラーに頻繁に遭遇しましたが、OCamlでは大丈夫でした。ただ、これは文法の差というよりは、慣れの問題な気がします。

参照型とアドレス比較

OCaml には = と == という二種類の演算子があり、 == はアドレス比較を行ってくれるため、無駄に参照型を使うという酷い手を使わずに済みました。OCamlの参照型はStandard MLの参照型とちょっとだけ違いがあり、直接的にパターンマッチの中に書くことが出来ません。両者の safeCar と safaCdr を比べると分かるかと思います。これはOCamlのrefはコンストラクタじゃないからだとかなんだとか。代わりに { contents = x } と書けばいけるとかなんとか。詳しくは詳しい人に聞いてください。

めっちゃ短い!

完成したコードを見てみるとなんと約280行。42個の言語のなかで恐らく最短です。本当に最短かどうかは後日調べます。
Standard MLで書いた時には let の end が6回登場しているので少なくても6行は短くなるとして、他にももう少し短くなっていることのなります。OCamlを書くときには慣れてしまったので短く書けるようになったというのもあるかもしれません。詳しい違いはソースコードを読んでください。

小学生並みの感想

たまには型の強い言語を使うのもそれなりに楽しいな、と思いました。

[LSP42] FantomとCeylon

金曜日, 12月 12th, 2014

(この記事はLISP Implementation Advent Calendar 12日目のためのエントリです。)

FantomCeylonでLISPを作りました。
https://github.com/zick/FantomLisp
https://github.com/zick/CeylonLisp

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその18〜19個目です。
Fantomという言語を選んだ理由はよく覚えていませんが、Wikipediaを眺めて見つけ、簡単そうなので選んだという感じだったかと思います。
Ceylonを選んだ理由もよく覚えていませんが、「JVMで動く(Java以外の)言語は簡潔で簡単なはず」という私の脳内で成り立つ法則に基づいて、JVMで動く言語を探して選んだような気がします。

Fantomの思い出

Fantomはなんといいいますか、フツーのイマドキの言語だったような気がします。正直なところ、非常に印象が薄いです。42個の言語のなかでも印象の薄さはトップクラスかと思います。良い見方をするなら不満に思う点がほとんどなかったのだと思います。

オブジェクト指向

Fantomはオブジェクト指向言語です。オブジェクト指向言語のなかでも「俺はオブジェクト指向言語だ! プログラムを書きたかったらまずクラスを作れ! ヒャッハー!!!」という私の好みじゃないやつです。これまで使った言語の中で言語の機能としてクラスを作ることができる言語はPython、Ruby、R、Factor、CoffeeScript、TypeScript、Dart、Falcon、Groovy、JSXと非常にたくさんありますが、このなかでLISPの1つの型と実装側の1つのクラスを対応させたのはRubyとDartとJSXの3つのみです。このなかでもJSXは1つの大きいクラスを作ってサブクラスはちょっといじるだけというインチキな手法を使いました。基本的に私はクラスというものがそれほど好きではなく、小さなLISPを作るだけならクラスを使わないほうが楽に作れると考えているくらいです。しかし、Fantomはクラスを作ることを強制される(トップレベルに関数を定義できない)言語なのでクラスを作らざるを得ません。

class LObj {
  Str tag
  Int? num
  Str? str
  LObj? car
  LObj? cdr
  LObj? args
  LObj? body
  LObj? env
  |LObj->LObj|? fn

  new makeNil() {
    this.tag = "nil"
    this.str = "nil"
  }
  new makeError(Str s) {
    this.tag = "error"
    this.str = s
  }
  new makeNum(Int n) {
    this.tag = "num"
    this.num = n
  }
  new makeSym(Str s) {
    this.tag = "sym"
    this.str = s
  }
  new makeCons(LObj a, LObj d) {
    this.tag = "cons"
    this.car = a
    this.cdr = d
  }
  ...
}

お察しの通り、1つしかクラスを作っていません。注目すべきは makeNil から makeCons までの部分です。一見メソッドの定義のように見えますが、すべて new というキーワードが付いています。実は、これらは「名前付きコンストラクタ」です。Fantomはコンストラクタに名前をつけることができるため、 makeError と makeSym のように同じ引数のコンストラクタも名前で区別することができるわけです。そう、今回はこの名前付きコンストラクタの機能を堪能するために1つしか(*)クラスを作りませんでした。名前付きコンストラクタの機能を堪能するために仕方なく1つしか作らなかったわけで、決して面倒だったわけではありませんよ。

(*) LISPのオブジェクトのために、という意味で他の目的はいくつかクラスを作ったよ、お兄ちゃん!

簡潔さ

Fantomはイマドキの言語故に、多くのものを簡潔に記述できます。文末にセミコロンなどのデリミタを付ける必要はありませんし、関数の型という複雑なものも |ArgType->RetType| のように簡潔に書けます。クラスのメンバにアクセスするときも this は省略可能ですし、右辺から方が分かる場合は ret := kNil のように型をつけなくても変数を作ることが出来ます。関数の戻り値の型は書く必要がありますが、無駄にコロンを書いたり、ましてや function などの長いキーワードを書く必要はないのでそれほど負担は感じません。やはり簡潔に記述できる言語は素晴らしいです。

Numeric tower?

Fantomには Int と Float というクラスがあって、どちらも Num というクラスを継承している。私はLISP感覚で「それじゃあ、Numを使っておけば整数も浮動小数点数も適切に扱えるな」と思っていたんですが、なんとびっくり。Numに対しては四則演算が出来ませんでした。何のためのNum型だよ。Fantom作った奴は何考えてるんだ、と憤りましたが、後で調べたところ、Javaも同じ仕様みたいです。流行っているんでしょうか。私は納得いきません。

Ceylonの思い出

Fantomとは逆に非常に印象深いのがこのCeylonです。Wikipediaをチラッと見た時から強烈な印象でした。

業界のアナリストからは同プロジェクトはJavaを抹殺するためのものだと言われている

Ceylon – Wikipedia

これを読んだときは何言ってるんだこいつ、と思ったんですが、LISPを作り終えることには、「ああ、『業界のアナリスト』らしい発想だなぁ」と思いました。

source/lisp/lisp.ceylon

GitHubのリポジトリを見れば分かるのですが、ファイルの概念のないScratchを除く、これまでの17個の言語はすべてリポジトリのトップレベルに単一のソースファイルをおいてきました。どれも400行前後のプログラムですから単一のファイルの方が分かりやすいでしょう。しかし、Ceylonはソースファイルを source というディレクトリの中に置かなければいけません。さらにその下にモジュール名に対応したディレクトリを作らなければいけません。Ceylonでプログラムを作るには最低でも1つはモジュールを作る必要があるので、要するに、最低でも2つはディレクトリを作らなければいけないということです。この時点で私はやる気を大幅に失ってしまいました。
一応言っておきますが、私はソースファイルを階層的に管理することを否定しているわけではありません。大きなモノをつくり上げるうえではどのように階層化するか、どうやってルールを守らせるかといったことが非常に大事になるのは分かります。でもそれを言語が強要するというのが嫌いなんです。helloworldのような数行で済むプログラムを作るのにもファイルの置き場を強要するような仕様が非常に嫌いです。

同名

Ceylonは多くのライブラリクラスを提供します。これ自体は非常に素晴らしいことですが、異なるパッケージに同名のクラスがあったり、パッケージと同名のフィールドがあったりします。例えば、 ceylon.file.Reader と ceylon.io.Reader 他にも ceylon.process と ceylon.language.process など。ドキュメントがこれらの名前を省略せずに書いてあれば困ることもありませんが、Ceylonのドキュメントは平気で Reader や process などという表記をしていて、どちらか区別できません。コード片を見て真似をしても動かず。クラスを調べたら使いたいメソッドがない。色々と調べた末に同名のクラスを見つける、という酷いハマりどころを用意しています。嫌がらせ以外のなにものでもないと思います。

Nullable

Ceylonは null を許す型(nullable)と許さない型(non-nullable)を区別します。これ自体は近代的で非常に素晴らしいことだと思います。が、 process.readLine を見て考えを改めました。この標準入力から1行取得する関数の型は String で、つまり null を許さないのですが、EOFを読み込むと null を返します。非常に基本的なライブラリの関数が基本的なルールを守っていないのです。あんまりだ。幸いなことに、 non-nullable な型を nullable な型に変換することはできるので、 wrapper をかませることで難を逃れました。

String? readLine() => process.readLine();

...

  // HACK: The return type of process.readLine is String but it can return
  // null when it reads EOF. I verified this code works on ceylon 1.0.0.
  if (exists line = readLine()) {
    ...
  }

numeric tower? 再び

Ceylonも Number というクラスがあるんですが、これも四則演算を実装していません。Fantomと同じです。まあ、JVMの上で動く言語なのでJavaと同じ仕様にするのは非常に自然なのでこれは仕方ないでしょう。

template classとunion type

Ceylonは非常に嫌な思い出が多いのですが、唯一楽しかったのがtemplate classとunion typeです。これについてはごちゃごちゃ書くよりコードを見たほうが早いと思うのでコードを載せます。

class ConsT<T>(T a, T d) {
  shared variable T car = a;
  shared variable T cdr = d;
}
alias SubrT<T> => Callable<T, [T]>;
alias ExprT<T> => [T, T, T];
alias DataT<T> => Integer|String|ConsT<T>|SubrT<T>|ExprT<T>;

class LObj(String t, DataT<LObj> d) {
  shared String tag = t;
  shared DataT<LObj> data = d;
}

alias Cons => ConsT<LObj>;
alias Subr => SubrT<LObj>;
alias Expr => ExprT<LObj>;
alias Data => DataT<LObj>;

ConsやSubrはLObjに依存するけど、LObjはConsやSubrに依存する。このような相互再帰的な型をtemplate classを使うことで表現できるわけです。さらにunion typeを使うことでLObjが持ちうるデータを明確に定義。この部分だけは書いてて面白かった記憶があります。 data の型は is というキーワードを使って確認できます。

LObj nreverse(variable LObj lst) {
  variable LObj ret = kNil;
  while (is Cons cons = lst.data) {
    LObj tmp = cons.cdr;
    cons.cdr = ret;
    ret = lst;
    lst = tmp;
  }
  return ret;
}

そのため、 tag は必要ないはずなんですが、なんで私が tag を付けたかは忘れてしまいました。恐らく消せるとは思うんですが、直そうという意欲はあまり湧きません。

※これは実装者の感覚によるもので速度を保証するものではありません

私がCeylonでLISPを書いた時に記したメモがあるんですが、非常に興味深い記述がありました。

型に関してはそれなりに楽しかったと思って実行してみたら想像を絶するくらい遅い。ヘタしたらScratchより遅い。

小学生並みの感想

不満がない言語より不満がある言語の方が印象に残るものなんだなぁ。

[LSP42] JSX

木曜日, 12月 11th, 2014

(この記事はLISP Implementation Advent Calendar 11日目のためのエントリです。)

JSXでLISPを作りました。
https://github.com/zick/JSXLisp

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその17個目です。
JSXという言語を選んだのは、やはり楽そうだったからです。以前、JavaScript系(*)の言語を使ったとき非常に楽な思いができ、もう一度あの時くらい楽が出来ないかなと考えていたら、JSXというJavaScript系の言語を見つけたので使わざるを得なかった訳です。

(*) ここでは、JavaScriptに変換できたりJavaScriptの代わりに使えるような言語のことだよ、お兄ちゃん!

外観

JavaScript系の言語だからきっと楽に書けるだろと思っていたらJSXのプログラムを見て度肝を抜かれました。

class Bar {
  function baz(str : string) : string {
    return  str + '!!!';
  }
  function qux() : string {
    return this.baz('foo');
  }
}
class Foo extends Bar {
  var field_ : string;
  function constructor(field : string) {
    this.field_ = field;
  }
  override function qux() : string {
    return this.baz(this.field_);
  }
}

class _Main {
  static function main(args : string[]) : void {
    var foo = new Foo('woohoo!');
    log foo.qux();
  }
}

工工工エエェェ(´д`)ェェエエ工工工
なんですか、このJavaに勝るとも劣らない冗長な記述は。関数には型を必ず付けないといけないのに function という非常に長いキーワードも書かなければならないというのがあまりにも理不尽です。 return がなければ戻り値の型が void なのは明らかなのに、それでもvoidと書かなければなりません。 function constructor というのもいい味を出しています。mainに着目してみると、かの有名なJavaの
public static void main(String[] args)
と、JSXの
static function main(args : string[])
が非常にいい勝負をしているのが印象的です。あとJSXはJavaと異なり this を必ず書かなければなりません。もしかすると、JSXはJavaよりも冗長なコードを書ける可能性を秘めた数少ない言語の1つなのかもしれません。

JavaScriptとの連携

JSXのマニュアルを読んでも標準入力から文字列を取ってくる関数が見当たりませんでした。まあ、JavaScriptに変換される言語なんだから仕方ありません。JSXはNode.jsで動くので、JavaScriptの関数を直接呼んでやれば解決だろうと思ったら、なんと、JSXがJavaScriptのデータを正しく扱えずにエラーになります。マニュアルをいくら読んでも回避策が分かりません。色々と検索してようやくやり方が分かりました。

// setEncoding と on をもつJavaScriptのオブジェクトを扱うための型
native __fake__ class Stdin {
  function setEncoding(str : string) : void;
  function on(str : string, fn : variant) : void;
}

JavaScriptのデータをうまく扱うために自分でインタフェースを定義してやらないといけないようです。まったく違う言語と連携するときには分かるんですが、なんでJavaScriptに変換される言語なのに、こんな面倒なものを記述しないといけないんですか。まあ、この記述は型安全のために我慢するとして、このやり方がマニュアルに載っていないというのはどういうことなんですか。いくらなんでもあんまりだと思います。

オブジェクト

文句ばかり書いてしまいましたが、ちゃんとLISPを作りました。今回はテーブルではなく、ちゃんとクラスを使ってLISPのデータ型を表現しました。

class LObj {
  var tag : string = 'NO TAG';
  function str() : string { return ""; }
  function num() : number { return 0; }
  function car() : LObj { return new LObj; }
  function cdr() : LObj { return new LObj; }
  function set_car(obj : LObj) : void {}
  function set_cdr(obj : LObj) : void {}
  function args() : LObj { return new LObj; }
  function body() : LObj { return new LObj; }
  function env() : LObj { return new LObj; }
  function fn() : function (:LObj) : LObj { return (args : LObj) -> args; }
}

class Nil extends LObj {
  static var nil : Nil = new Nil;
  function constructor() { this.tag = 'nil'; }
  override function str() : string { return "nil"; }
}

class Num extends LObj {
  var num_ : number;
  function constructor(num : number) {
    this.tag = 'num';
    this.num_ = num;
  }
  override function num() : number { return this.num_; }
}

すべてのベースとなるLObjにすべての型のためのアクセッサが定義されているというおちゃめな実装です。クラスの種類ではなく文字列の tag でデータ型を区別します。「このクラスは食べられないよ。明日もう一度この場所にきてください。本当のオブジェクト指向を見せてあげましょう。」という方のために、もう少し普通っぽい実装も試してみました。

class LObj {}

class Nil extends LObj {
  static var nil : Nil = new Nil;
}

class Num extends LObj {
  var num_ : number;
  function constructor(num : number) {
    this.num_ = num;
  }
  function num() : number { return this.num_; }
}

...

  if (obj instanceof Cons) { ... }

...

  var num_obj = obj as Num;
  return num_obj.num();

はい。クラスはずいぶんスッキリしました。型の比較は文字列でなくなったので、型名のtypoなどはコンパイルで弾けます。さて、これを実行してみると、なんと驚きの結果。本のプログラムより1.5倍ほど遅くなります。細かい要因などは調べていませんが、どうも instanceof が特に遅いように見えます。詳しくは詳しい人に聞いてください。

小学生並みの感想

細かいところはともかく、マニュアルはしっかり書いて欲しいです。

[LSP42] Groovy

水曜日, 12月 10th, 2014

(この記事はLISP Implementation Advent Calendar 10日目のためのエントリです。)

GroovyでLISPを作りました。
https://github.com/zick/GroovyLisp

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその16個目です。
Groovyという言語を選んだのは、ずばり楽そうだったからです。毎週末頑張ってLISPを書いていたのにまだ全体の3分の1を超えた程度という事実に疲れたら、ふと「JVM上で動くJava以外の言語はみんな『Javaと較べてこんなに簡潔です』と言ってるし簡単に書けるに違いない」という(根拠の無い)発想にいたり、名前とJVMで動くということしか知らないGroovyを選びました。

外観

さて、実際にはどれくらい簡潔だったかと言いますと、

def makeCons(a, d) {
  return [tag: 'cons', car: a, cdr: d]
}

こりゃ簡潔だ。何よりも型を書かないのがいいですね。

rangeの罠

イマドキの言語にはよく部分配列や部分文字列を取り出すための構文があります。Pythonだと str[start:end] だとかRubyだと str[start...end] といった具合です。Groovyにも同様に str[start..end] という構文があります(ただし、 end は inclusive)。また、 start から終端までの部分配列/文字列を取得する場合は str[start..-1] と書けるのですが、これにはPythonやRubyにはない罠がありました。

if (str[0] == kLPar) {  // 先頭の文字が括弧なら
  return readList(str[1..-1])  // 残りの文字列をリストとして読み込む
}

一見正しそうなコードですが、 str の長さが1の場合、なんと out of range 扱いで例外を投げます。これはおかしい。 「だってstr[1]は取れないでしょ」と思った人は「部分」という言葉についてもっと考えるべきです。この仕様は不自然ですし何よりもメリットを感じません。誰かこの仕様の意味を教えて下さい。

スコープの罠

Groovyはその見た目からいってどう見てもイマドキの言語。PythonやRubyみたいなものだと思って書いていました。そうしたら見事罠に引っかかりました。

def foo() {
  bar = 1
  ...
}

こんなコードを書いていたのですが、Groovyではこの場合 bar はグローバル変数として扱われます。ローカル変数として扱いたければ def を付けて

def foo() {
  def bar = 1
  ...
}

と書きます。知っていればそれだけの話で、「JavaScriptのvarと同じだろ」と言ったところなのですが、マニュアルをよく読んでいなかった私は見事にしてやられました。言い訳をしておくと、Groovyのマニュアルが読みにくいのが悪いです。あと、スコープという大事な話を下の方に書いているのもいただけない。そんな下の方読みませんよ。

小学生並みの感想

普通に使いやすかったので言いがかり的な文句くらいしか書くことがありませんでした。

[LSP42] FalconとEuphoria

火曜日, 12月 9th, 2014

(この記事はLISP Implementation Advent Calendar 9日目のためのエントリです。)

FalconEuphoriaでLISPを作りました。
https://github.com/zick/FalconLisp
https://github.com/zick/EuphoriaLisp

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその14〜15個目です。
FalconとEuphoriaという言語は、Wikipediaを眺めているときに見つけ、簡単そうだったので選びました。今回の企画でLISPを作った言語は10を超え、過去にLISPを作った言語も合わせると既に20を超えており、正直、使う言語を探すほうがLISPを作るのよりも疲れるようになってきました。

Falconの思い出

久々のbegin/end系の言語です。相変わらず好きになれません。でもFalconには1行で書く場合に end を省略できるという素敵な文法があるのでいい感じです。例によってテーブル(Falconの言葉だとdictionary?)でLISPのデータ構造を表現しようとしたのですが、テーブルを比較すると、内容比較になってしまいます。これでは eq を作ることが出来ません。色々と試行錯誤したところ、一度クラスで包んでやればアドレス比較になるようなので、その方法で逃げました。

class L(tag, dict)
  tag_ = tag
  dict_ = dict
  function __getIndex(index)  // 演算子オーバーロードだよ、お兄ちゃん!
    return self.dict_[index]
  end
  function __setIndex(index, value)  // これもだよ!
    self.dict_[index] = value
  end
  function tag()
    return self.tag_
  end
end

function makeCons(a, d): return L('cons', ['car' => a, 'cdr' => d])

function safeCar(obj)
  if obj.tag() == 'cons': return obj['car']
  return kNil
end

努力の方向が間違っているような気もしますが気のせいです。全部クラスで作れという苦情は受け付けておりません。

Euphoriaの思い出

好きになれないとはいえ、endと書くことはある程度慣れてきました。しかし、そんな私の気持ちを打ち砕いたのがこのEuphoria。

function safeCar(object obj)
  if TagEq(obj, "cons") then
    return Car(obj)
  end if
  return kNil
end function

なんですか、この end function というやつは。関数を書くたびに function と2回もタイプするとか正気の沙汰とは思えません。そんな時間があったらどれだけの括弧が書けると思っているんですか。
お察しの通り、またテーブル(Euphoriaの言葉だとmap)でLISPのデータ構造を表現したんですが、mapの操作も冗長だったので、取り敢えず wrapper を書いて逃げました。

function L(sequence tag, object data)
  map obj = new()
  put(obj, "tag", tag)
  put(obj, "data", data)
  return obj
end function

function Tag(object obj)
  return get(obj, "tag")
end function

function TagEq(object obj, sequence str)
  return equal(Tag(obj), str)
end function

function makeCons(object a, object d)
  return L("cons", {a, d})
end function

努力の方向性(冗長性の除去のため以下略)。
あと、一部のオブジェクトは copy-on-write みたいなので注意が必要です。

function SetCar(object obj, object val)
  object data = Data(obj)
  data[1] = val
  put(obj, "data", data)
  return val
end function

うーん、冗長。マニュアルをしっかり読んだわけではないのでなにが copy-on-write なのかはよく分かりません。

小学生並みの感想

タイプ数が少なくてすむ言語は正義だと思いました。

[LSP42] CofeeScriptとTypeScriptとDart

月曜日, 12月 8th, 2014

(この記事はLISP Implementation Advent Calendar 8日目のためのエントリです。)

CoffeeScriptTypeScriptDartでLISPを作りました。
https://github.com/zick/CoffeeLisp
https://github.com/zick/TypeLisp
https://github.com/zick/DartLisp

どれもJavaScriptに変換可能でブラウザで実行できる言語です。ただし、Dartは独自のVMも持っています。

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその11〜13つ目です。
CoffeeScript、TypeScript、Dartという言語を選んだのはJavaScriptの派生(?)言語ということで非常に簡単そうだったからです。正直これらJavaScript系言語は簡単すぎるので敬遠していたのですが、42個中10個の言語で実装するだけで既に7週間程度を費やしていたため、これはもう少しスピードアップしようと思い、久しぶりに一日で3つの言語でLISPを実装しました。

CoffeeScriptの思い出


PythonとJavaScriptを混ぜたような感じです。インデントでブロックを表し、タイプ数が少なくてすむような文法です。例によってテーブル(JavaScript系の言語なので恐らくオブジェクト?)を使ってLISPのデータ構造を表しました。

makeCons = (a, d) ->
  { tag: 'cons', car: a, cdr: d }

TypeScriptの思い出

CoffeeScriptが独自の文法なのに対し、TypeScriptはJavaScriptのスーパーセットとなっています。名前が表す通り、変数や関数に型をつけることができます。型は optional なのですが、型をつけなければ完全にJavaScriptになってしまうため、頑張って型をつけました。

// tag は必須、後のデータはあってもなくてもいい
interface LObj {
  tag: string;
  str?: string;
  num?: number;
  car?: LObj;
  cdr?: LObj;
  fn?: (args: LObj) => LObj;
  args?: LObj;
  body?: LObj;
  env?: LObj;
}

function makeCons(a: LObj, d: LObj) {
  return { tag: 'cons', car: a, cdr: d };
}

ひたすらに色んな所に LObj と書くだけの簡単なお仕事でした。実質的に型付けの恩恵を受けれない上に、やたらとタイプ数が増えるだけの悲しい思いをしました。CoffeeScriptの後だったので、 function と書くだけでも面倒だったのに。あとコンパイル時間が長いのもいただけない。

Dartの思い出

DartはCoffeeScriptと同様に独自の文法を持っています。ブロックは中括弧{}で表し、セミコロンも必要なため、CoffeeScriptと比べるとタイプ数は多くなりますが、それでもイマドキの言語らしく、少ないタイプ数で色々できます。例によってテーブル(Dartの言葉だとmap)を使ってLISPのデータ構造を表現しました。

makeCons(a, d) => {
  'tag': 'cons',
  'car': a,
  'cdr': d
};

が、完成品を走らせてみるとCoffeeScriptとTypeScriptと比べびっくりするくらい遅い。ちょっと遅いというレベルではなく圧倒的に遅い。「なんでこんなに遅いの?」と疑問を書いたらDartの中の人から「クラスを使うんだ!」というメッセージとともにパッチが送られてきました。

class Cons {
  var car;
  var cdr;
  Cons(this.car, this.cdr);
}

makeCons(a, d) => new Cons(a, d);

こんどはDartが圧倒的に速いという結果に。でも中の人にアドバイスを貰ったのはちょっと反則気味な気も。

小学生並みの感想

みんなちがって、みんないい、わけでもない。

[LSP42] NekoとTcl

日曜日, 12月 7th, 2014

(この記事はLISP Implementation Advent Calendar 7日目のためのエントリです。)

NekoTclでLISPを作りました。
https://github.com/zick/NekoLisp
https://github.com/zick/TcLisp

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその9〜10個目です。
Nekoを選んだ理由はよく覚えていませんが、たしかWikipediaを眺めていて名前が可愛かったから選んだという感じだったかと思います。
Tclを選んだ理由は簡単な予感がしたのと、このマンガがいい感じだったからです。

Nekoの思い出

外観

見るからにイマドキの言語ですぐ書けそうです。

var makeCons = function(a, d) {
  return { tag => "cons", car => a, cdr => d }
}

こりゃどう考えても簡単だ。

マニュアル分からん

でもライブラリ関数を使おうとしたところでいきなりつまづきました。たぶん、何かを import やらなんやらしないといけない、というのはすぐに予想がついたんですが、いくらマニュアルを探しても分からない。仕方がないからぐぐってみたものの、マイナー言語故にあまり資料が見つかりませんでした。頑張って色々調べた末に、

var buffer_add = $loader.loadprim("std@buffer_add", 2);

こんなふうに書けばいいと分かりました。この 2 は引数の数。なんでマニュアルに載ってる関数使うために、FFIを使うみたいな真似をしないといけないとは。いや、それはいいとして、せめてマニュアルの分かりやすいところに書いてください。

謎の『$』

Nekoの一部の組み込み関数は $print という風に先頭に $ が付きます。すべての関数に $ が付くのなら分かりやすいのですが、一部にのみ付くので、頻繁に付け忘れました。なんとかして欲しいです。

スクリプト言語?

Nekoのマニュアルをいくら探しても標準入力から1行とってくる、という関数が見つかりませんでした。しかたがないので自分で実装するはめに。

var readline = function() {
  var stdin = file_stdin()
  var buf = buffer_new()
  try {
    while (true) {
      var c = file_read_char(stdin)
      if (c == 0x0a) {
        break;
      }
      buffer_add_char(buf, c)
    }
  } catch e {
    return false
  }
  return buffer_string(buf)
}

なんでスクリプト言語で readline なんつーものを自分で実装しないといけないんですか。やめてください。

再帰不能

Nekoでは素直に関数の再帰呼び出しを行うことができないようです。

var fact = function(n) {
  if (n == 0) {
    return 1
  }
  return n * fact(n - 1)  // Uncaught exception - Invalid call
}

なんと例外を投げます。どうやら function の内側からは fact が見えないようです。

var fact = null
fact = function(n) {
  if (n == 0) {
    return 1
  }
  return n * fact(n - 1)  // Uncaught exception - Invalid call
}

こんな風に書きなおしても駄目。内側の fact は null になってしまいます。関数を定義し終えた後に変数を書き換えても元の値が使われてしまうようです。
では変数を書き換えなければいいのだろうということで、オブジェクトを作ってそのフィールドに関数を入れることで解決。

var fact = $new(null)
fact.call = function(n) {
  if (n == 0) {
    return 1
  }
  return n * fact.call(n - 1)
}

またどこかで聞いたような解決策に。

Tclの思い出

外観

パッと見たところ、割と普通の言語に見えます。

proc skipSpaces {str} {
    set i 0
    while {$i < [string length $str]} {
        if {![isSpace $str $i]} {
            break
        }
        incr i
    }
    return [string range $str $i [string length $str]]
}

でもよく見てみると、やたらと大括弧[]が多いことに気付きます。これこそがTclの(私にとって)面白いところです。

コマンド

Tclのプログラムは「コマンド」と引数の並びです。コマンドとは先程のプログラムだと set とか while とかです(実は proc もコマンドですが)。大事なルールは「引数は基本的に評価されない」ということです。 set i 0 の i は評価されずに名前のまま残るからこそ、 変数iに 0 が代入されるわけです。 incr i も同様に変数iがインクリメントされるわけです。
引数に現れる変数を評価したければ変数名の前に $ を付けます。しかし、これは変数のみを評価するのであって、引数全体を評価するものではありません。例えば string length $str というプログラムは $str の部分は変数の値に評価されますが、 length という部分はそのまま名前として残ります。結果として、 string というコマンドに length という名前と変数strの値が渡されます。実行結果は変数strの値である文字列の長さになります。lengthはコマンドstringのサブコマンドとでもいったところでしょうか。

コマンド置換

引数を評価したければ、大括弧を使います。例えば set i [string length $str] とすると、iに文字列の長さが代入されます。大括弧で囲まれた部分はコマンドとみなされ、それが先に実行されて、その実行結果で引数が置き換えられるので「コマンド置換」などと言うらしいです。シェルスクリプトのバックスラッシュみたいなものと考えれば分かりやすいかもしれませんが、なんだか品がありませんし、何よりも面白くありません。
ここで考え方を変えて、「Tclのプログラムは全体が quote されていて、大括弧で unquote される」と考えてみてください。Tclのプログラムが急に美しい世界に見えてきます。初めてLISPのquoteを知った時のような興奮が押し寄せてきます。何を評価して何を評価しないかは人に委ねるのではなく自分で決めるんだと考えると興奮のあまり鼻血が出てきそうになってきます。

リスト

Tclにはリストがあります。「そりゃリストくらいあるだろ」とお思いかも知れませんが、「リストは中括弧{}で表す」と聞くと、なにか大変なことに気付きませんか。そうです。ifやwhileの後に現れる中括弧、さらにはprocの後に現れる中括弧はなんとリストだったのです。Tclはすべてが quote されているのですから、リストもquoteされたリストです。つまり、リストで「その場では実行されないプログラム」を表現できるわけです。procやifやwhileはその引数を必要な場合に適切に評価するわけです。つまりスペシャルフォームです。もはやTclのプログラムは美しい花畑に見えてきて、その世界にいるだけで幸せな気分になってきます。「そもそもスペシャルフォームの存在が美しくない」という純粋すぎる方は専門の医師の診断を受けたほうがいいかもしれません。

expr

前述のプログラムをしっかりと読んだ人は $i < [string length $str]![isSpace $str $i] という箇所はコマンド+引数の形になっていないと気づいたかもしれません。「美しい世界など幻想。所詮は汚いスクリプト言語だ。」と思ったら大間違いです。
Tclには expr というコマンドがあります。これは引数を式とみなしてその値を求めるものです。

expr 1 + 2  # => 3
set i [expr 1 + 2]  # iに3が代入される
string length "abcd"  # => 4
expr $i < [string length "abcd"]  # => 1 (Tclで1は真の扱い)

もうお分かりですね。ifやwhileの第一引数として受け取ったリストは expr に渡されるわけです。スペシャルフォームの枠組みからは出ておらず、十分に美しい世界の中の話といえるでしょう。興奮冷め止まない感じです。

TclでLISP

さて、そろそろLISPを作った話をします。普段ならその言語にどんなデータ構造があってどうやったら楽に書けそうかを考えるんですが、すっかり興奮しきったは私はLISPを書きたい欲求を抑えきれず「もう整数だけあればいいや」とまるでCで書くようなLISPを書いてごみ集めも自分で書いてしまいました。

# <Object>
#   MSB              LSB
#   gc(1) data(28) tag(3)

proc getTag {x} {
    return [expr {$x & 0x7}]
}

proc setData {x data} {
    return [expr {($x & ~0x7ffffff8) | (($data << 3) & 0x7fffffff)}]
}

基本的にScratchのときと同じ実装です。ただ、マウスではなくキーボードでプログラムを書けるのであまりにも簡単でした。キーボードの神様に感謝。

小学生並みの感想

キーボードでプログラムを書けるって素晴らしい。

[LSP42] Io

土曜日, 12月 6th, 2014

(この記事はLISP Implementation Advent Calendar 6日目のためのエントリです。)

IoでLISPを作りました。
https://github.com/zick/IoLisp

Ioは prototype-based のオブジェクト指向言語でSmalltalkの影響を強く受けています。多分。でも私はSmalltalkのことをよく知らないのでなんとも言えません。

動機

今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその8つ目です。
Ioを選んだ理由は同僚に「なかなか面白そうな言語だから」と推薦されたからです。触れば触るほど面白さが伝わってくる言語でしたが、面白さが全部伝わる前にLISPが完成してしまいました。

文法

Ioの文法は初めて見るとちょっとだけ不思議な感じがします。

fact := method(n,
  if ( n == 0,
    1,
    n * fact(n - 1)))

注目すべきは method(引数, 本体)if (condition, then, else) という部分です。LISPとCを折衷したような独特な文法です。少々風変わりですが慣れてしまえば書きやすいです。たまにカンマを書き忘れて、それで挙動が変わってしまうことがあるのがちょっと困りモノです。

オブジェクト

Ioはいわゆる prototype-base のオブジェクト指向言語なのでクラスは存在しません。

LObj := Object clone
makeCons := method(a, d,
  cell := LObj clone
  cell tag := "cons"
  cell car := a
  cell cdr := d
  cell)

既存のオブジェクトのクローンを作って新しいオブジェクトを作っていくわけです。「俺はオブジェクト指向言語だ! プログラムを書きたかったらまずクラスを作れ! クラスを作るたびにファイルを作れ! ヒャッハー!!!」などといった横暴な言語と違って、穏やかな優しい印象を受けます。「なんで tag とか作ってるの? 別にいらないでしょ?」とか思った人は後で職員室に来るように。

高階関数

SUBRを作るために高階関数を作ろうとしたら少し困りました。

f1 := method(arg, body)
f2 := f1

などと書いても f2 には nil が代入されてしまいます。こうなる理由は調べた気がするんですがもう忘れてしまいました。回避策としてオブジェクトのスロットにメソッドを代入して、そのオブジェクトを持ちまわることにしました。少し考えてみたらまったく同じことを過去にもやったことを思い出しました。RubyでLISPを実装した時です。さらに考えてみると、RubyもSmalltalkの影響を受けている言語です。これを踏まえてマニュアルを読み直してみると、SmalltalkにもRubyにもIoにも block という概念があるのに気付きました。

「そうか、わかったぞ!」

と、思ったところまでは覚えているのですが何を分かったのか忘れてしまいました。誰か思い出させてください。

職員室

Ioの オブジェクトは hasProto というメソッドを持ち、これでオブジェクトがどのオブジェクトを元に作られたが調べられるので、わざわざ tag などといったものを付けなくても実行時に型を調べられるわけです。

Cons := LObj clone
makeCons := method(a, d,
  cell := Cons clone
  cell car := a
  cell cdr := d
  cell)
...
if (obj hasProto(Cons), ...)

しかし、これを使ってLISPを書くと速度が低下しました(6.2sec -> 6.6sec)。恐らく、 prototype のチェーンをたどるより文字列を比較するほうが速いのが要因でしょうが、原因の究明は読者の練習問題とします。

小学生並みの感想

やっぱり書きやすい言語はいいと思いました。