今日のまめ知識

ドヤにある扇風機のコンセントはこんな形をしている。

概要

言語処理的なものを作る上において、最小限のサブセットとして、TokenizeとParse、そしてそれに付随する環境について一通り学ぶことのできる最小のLispというものを考えていた。そして、それをプロトタイピングした結果、Unlispというものができたので、それを共有する。

はじめに

いろんな本をかいつまみながら勉強していたりしていると、「ウォー、言語を実装しなければ、じゃないと面倒だー」ということになったりするのだけれども、実際に作ってみると、だいたい付け焼き刃であったり、どうしようもないことで頓挫したりする。なので、「とりあえず最小の言語っぽいものを作るとしたらどんな風になるのか」みたいなことを、Rubyで考えたりしていた。できたのは、Specで見せるとこんな感じ。

上の画像、間違いがあって、正確には fib 7 のときに13になるのでした。

前提

とはいえ、全部の文字列を愚直にパースはしない。ここでは、実装したい何らかの言語において、List型か、String型を入れることのできるListで表現する。言語によってはListでは無く、Arrayとも言うけれど、そういうデータ形式である。Rubyでいうと下のような感じ:

[ " + " , [ " + " , " 1 " , " 1 " ]]

この方針は特別なものではなく、例えば、達人出版会から配布されている『つくって学ぶプログラミング言語 RubyによるScheme処理系の実装』では、この方針が取られているし、またこの文章よりもちゃんとしたプロジェクトであるminiMALでは、配列を利用している。従って、最小限のリスト表現に関しては、その言語のリスト表現にのっとる形としている。

サポートするプリミティヴな型

サポートする型については、 Integer と Function のみであるとする。

この思いきりは簡単で、 String を型としてサポートするとその操作も同時にサポートしなくてはならないという冗長さが生まれてしまい、それは実装本位という意味では余計なものであるため、排除する。また、 Boolean に関しても、実質Pythonなどの言語は、 0 とそれ意外を True と False に当てているので、これで代用する。

サポートするビルドインの式

サポートされる関数に関しては、 < と = という真偽値を判定する式、 + と - という数字を操作する関数、 println という式の結果を表示するものだけをサポートする。

ただ、言語が実装されるにも関わらず、 Hello, World. が打てないのはもったいないので、オプションとして putchr みたいな、数字を文字で出力するオプションがあってもいいかもしれない(これは怠惰のため保留中)。

また < および = は、正しければ 1 を、間違っていれば 0 を返す関数と定義する。これは、 Boolean が存在しないことによるためである。

サポートする特別フォーム

上記のような関数が、引数の結果をまず先に計算するのに対して、引数の結果が必ずしも先に計算されるようなことがない文法のことを、特別フォームと呼び、区別する。そして、Unlispで実質存在しているのは if 、 def 、 fn のみである。そして、以下がその構文である:

(if <predicate> <consequent> <alternative>) (fn <formal parameter> <body>) (def <name> <body>)

まず、 if に関しては、 <predicate> の値が定まるまで、 <consequnt> の値も、また <alternative> の値も評価しない。 <predicate> の値が定まった場合、もし1であるならば、 <consequent> を計算し、その値を返す。また、0であるならば <alternative> を計算し、その値を返す。

fn は一般的に匿名関数等で呼ばれるような、名前の無い関数を作りだす。

((fn x (+ x x)) 1) # => 2

細かい読者ならば、 fn の一般形において、 parameters という複数形が使われていないことに注目すると思う。 fn において、取る引数は実質1つとして考えられている。というのは、n引数は、1引数に直すことが可能であるためだ。また、このようにすることで、環境に変数のスコープを内包する時に、一変数だけのスコープを所持すればいいことになり、実質なんらかのペアによって、その変数を確保すればいいことになる。

また、 def に関しても、直接関数を定義するような糖衣構文を所持していない。これは、Schemeにおいて

( define square ( lambda ( x ) ( * x x )))

で実質関数が定義できることから習っている。従って、 def からは直接関数を定義することはサポートしない。

仕様メモ

ここで慧眼な読者であるならば、例えば他の < であったり、あるいは + といったような関数の役割を果すような関数がn引数を取ることに関して、整合性が取れていないという指摘があるかと思うけれども、これは実際その通りだと認めざるを得ない。これらを高階的に使用する場合において、現状の実装は破綻していると言わざるを得ない。

これは現状の実装においては考慮しなかった点であり、今後解決すべき課題と言える。

実装

これに関しては、UnLispを実装するにあたってのメモ書きである:

Parser部分

実装に関しては、 Tokenize 、 Parser という二つを用意し、各 String を、 Integer と Atom と List と Error という種類に分ける。 Atom は変数名に値するものである。

Parser の段階で、コードの解析がおこなわれる。実装としては、 Atom が fn であった場合、上記の構文に当てはまるならば、 Funciton という型をあて、 Token にする。このように、常に何らかの式に関しては Token にして渡すようにする。

基本的に、リストの最初は呼びだす関数であるということになっている。しかし、例えば、最初が fn だった場合、最初に受けるトークンは List としているため、これを展開して、関数としなくてはならない。例としては下の場合がそれに当たる:

[[ " fn " , " x " , [ " + " , " x " , " 1 " ]], " 6 " ]

なので、リストがやってきたとき、これを展開するようにする。

また、引数の場合にもリストがやってこないとは限らないので、これもまた展開する。基本的にはリストは組み込みである ' を除き、全て展開する。

関数部分

Unlispにおいて、関数を定義する場合、引数は一つということであった。これは同様にクロージャが必要になるということでもある。

Unlispの現実装では、関数は関数が実行されたときの環境を、関数の中で保持し、これを利用する。そして、実行が終了したさいに、関数は元の環境を返す(環境については後出する)。このようにして、クロージャは上手く実行するようになる。

環境

プログラミング処理系において必要となるのは、まず環境という考えかただろう。先程代入された変数は、何らかの形で何処かに、そのデータを保持しておかなくてはならない。そのために必要になるのが環境である。

UnlispのRuby実装の場合、これを配列とコードのペアとして保持している。ユーザー定義における関数は、全て一引数の関数のため、これで十分である(もし、n引数の関数を実装するならば、リストのリストといったように検索していく必要があるけれど、よく考えてみれば、実装自体の手間はそれほどかからない)。

また、クロージャや再帰の場合、新しい値をストックしていく。具体的に言うならば、次のようなコードの場合を考察してみよう:

[ " do " , [ " def " , " upto " , [ " fn " , " x " , [ " if " , [ " < " , " x " , " 10 " ], [ " upto " , [ " + " , " x " , " 1 " ]] " x " ]]], [ " upto " , " 5 " ]]]

このとき、何が環境で起きているかといえば、順次 x をスタックしていき、そして脱出するときに、その環境を戻している。具体的な動作は次のようになる。

---> IN [[- : x, - Integer: 5]] ---> IN [[- : x, - Integer: 5], [- : x, - Integer: 6]] ---> IN [[- : x, - Integer: 5], [- : x, - Integer: 6], [- : x, - Integer: 7]] ---> IN [[- : x, - Integer: 5], [- : x, - Integer: 6], [- : x, - Integer: 7], [- : x, - Integer: 8]] ---> IN [[- : x, - Integer: 5], [- : x, - Integer: 6], [- : x, - Integer: 7], [- : x, - Integer: 8], [- : x, - Integer: 9]] ---> IN [[- : x, - Integer: 5], [- : x, - Integer: 6], [- : x, - Integer: 7], [- : x, - Integer: 8], [- : x, - Integer: 9], [- : x, - Integer: 10]] <--- OUT [[- : x, - Integer: 5], [- : x, - Integer: 6], [- : x, - Integer: 7], [- : x, - Integer: 8], [- : x, - Integer: 9]] <--- OUT [[- : x, - Integer: 5], [- : x, - Integer: 6], [- : x, - Integer: 7], [- : x, - Integer: 8]] <--- OUT [[- : x, - Integer: 5], [- : x, - Integer: 6], [- : x, - Integer: 7]] <--- OUT [[- : x, - Integer: 5], [- : x, - Integer: 6]] <--- OUT [[- : x, - Integer: 5]] <--- OUT [] <--- OUT

このようにしている理由としては、次のようなコードを考えるとわかりやすい(『つくって学ぶプログラミング言語 RubyによるScheme処理系の実装』の例による):

[[ " fn " , " x " , [ " + " , [[ " fn " , " x " , " x " ], " 1 " ], " x " ]], " 2 " ]

このとき、トップレベル環境の書きかえモデルを採用した場合、一番最後の x の値、つまり 1 が採用されてしまうといった問題が出てくる。また、実装のさいに、関数から離脱したさいには、関数の中にあった変数は使えないで欲しいという問題が出てくる。そうした場合、スタックモデルは有効である(ちなみに、この話は『プログラミング言語の基礎概念』でも取りあげられている)

おまけ

SICPを読みといてみると、4章にて「cond」を「if」に展開するという話題が出てくる。これは、特殊形式のフォームを増やさないためだということである。なので、それに習って、「if」だけにしている。

得られた知見

配列はつらい

最初にリストをそのまま使えば楽できるんじゃね?という話をしたが、Rubyだったり、Pythonの場合、配列が参照になっていたりするため、式を置きかえながら処理していると、他のところに副作用がおよんでバグが出たりして良くなかった。元のコードを保持するために、 lst.clone みたいなコードを挟むだけで、一発で正常に動くところが多数あったりしたので、配列操作で他と共有して使う場合にはいじらないほうがよい。

こう考えると、共有されているだろうリストを直接弄るのはバッドノウハウで、本来ならば弄れないように、変更不可にする必要があって、そういう意味で、そのようなリストの変更不可能性を担保しているのはとても重要なことだった。

継続っぽいやつ

上記にからむことで、最初置きかえモデルを使っていたために、関数トークンみたいなのを途中で生成して、自分が計算途中の環境を保持していたために、下のような妙な挙動をしていた:

端的に言ってしまえば、これは同じ計算途中の関数を何度も参照しているためにおきたバグなのだが、これをもう少し普遍化すれば「もしかしてこれが継続ということになるんじゃないのか」ということに気がついた。さすがに継続まで実装することになるとなかなか辛いことだけれども、しかしこういうバグによって、別の実装についてのヒントが得られることは凄いことだ。プライベートなコードならバグは出し放題なので、どんどんバグを出していきたい。

都市伝説によれば、クロージャも元はSchemeのバグで、意外に使えるからそのままにしておいたという話を聞いているけれど、正確には不明。

Rustの所有者ってもしかして便利っぽい

今回、処理系もどきっぽいものを作ると、いろんなところにリストの要素がたらいまわしにされてグチャグチャになったりしていた。

で、ちょっとRustも仕様が落ちついてきたみたいだし、少しドキュメントでも覗いてみるかーと思ってのぞいてみると、所有権という概念があったりして、最初これについてピンとこなかったんだけど、こうやってたらいまわしになるようなデータだったりすると、「いったいお前は何処でなにをしているんだ〜」とシンガーソングしてしまうので、なるほど、こういうのが重要なんだなと思ったりした。

どうでもいいこと

もちろん、リスプっぽいコードを配列で表現するんだけど、ところどころでコロンを忘れてポコーってなってしまっていた。リスプ書くぞ〜ってなっているときには、リスプ器官が覚醒するんだなと思って面白かった。

おわりに

これだけ単純な仕様なのだけれども、プログラマとしての技術力が無い自分には4日かかった。とはいえ、実際に作ってみると、本当にこんな単純なものであっても、「俺はもうだめだ」「これだったら取りあえずは解決できるが、納期が無いのに妥協しても仕方ないからもうすこしだけ頑張ろう」といったことが渦巻き、プログラミング言語制作者には尊敬の念を覚えずにはいられなかった。もうすこし勉強をして、手癖でこういうのが書けるように、練習していきたい。余裕があれば、他の言語でも実装しようと思う。あとはSICP4章をもう少し勉強せなあかんなーとも思った。

ソース