このところ、趣味でプログラミング言語を作っています。
どんな言語を作っているか
静的な型付けの関数型プログラミング言語で、見ためはこんな感じです。
# hello, world!
puts "hello, world!"
# 1 * 2 * .. * n を計算する関数
fun factorial : Int -> Int = {
1 -> 1
n -> n * factorial (n-1)
}
factorial 5 #=> 120
型について理解したいという気持ちがあったので、型検査や多相型など、型に関する機能を少しづつ取り入れて作っています。
プログラミング言語 Mud
http://mud.tiny-app.net/
なぜ作っているか
純粋に楽しみとしてです。世の中にまともな言語がたくさんあるのに、今さらおもちゃみたいなものを新しく作る意味ないよねとも思いますが、どんな分野でも自分だけのちょっとしたものを自作するのはやっぱり楽しいものです。
あと既存の言語についての勉強にもなります。
どんな言語機能が欲しかったか
最初の動機となったのは、「関数型の言語で、関数適用の関数名と第一引数の順番を “.” でひっくり返せばオブジェクト指向っぽく見えるんじゃないか」という思いつきでした。
関数型の言語では、以下のような式を書くことになるケースがあると思います。
reverse (uniq (sort [2,1,4,1]))
#=> [4,2,1]
意味としては、 [2,1,4,1] というリストをソートして、重複を取り除いて、逆順にするということになります。
ですが、ふつうに左からコードを読むと、「ひっくり返す。なにを? 重複を取り除いたものを。なにから?・・」となって読みづらいことになります。意味をつかむためには式を右から左に、逆順に読まなければいけません
それがめんどくさいよねということで、既存の言語、たとえば Elixir だと |> 演算子があって、関数呼び出しの引数と関数名の位置をひっくり返すことができます。つまり
sort [2,1,4,1]
を
[2,1,4,1] |> sort
と書くことができる。なので、これを繰り返すと元の式はこうなります。
[2,1,4,1] |> sort |> uniq |> reverse
「[2,1,4,1] というリストをソートして、重複を取り除いて、ひっくり返す」という素直な語順どおりになりました。
でも思ったのです。これ |> じゃなくて “.” なら普通にオブジェクト指向っぽい見た目になるじゃんと。つまりこういうことです。
[2,1,4,1].sort.uniq.reverse
ふつうに Ruby で動きそうな見た目になりました(実際動きます)。
なので、こんなふうに、関数型の言語なのに見ためはオブジェクト指向っぽく書ける言語を作ってみようと思ったのがきっかけでした。
(あとから知りましたが、そういうアイデアの言語はすでにいっぱいあるようです。Nim や、あるいは Ruby のまつもとゆきひろさんの Streem がそうです。でもそういう人たちと同じアイデアを思いつけたのは嬉しいです。 )
シンプルな関数定義
Haskell の関数定義の構文がとてもシンプルで、好きです。たとえば 1 * 2 * 3 * .. * n を関数を計算する関数 factorial は、こう書けます。
factorial :: Int -> Int
factorial 1 = 1
factorial n = n * factorial (n-1)
そして Elixir の匿名関数の構文もまたシンプルでかっこいいと思います。
factorial = fn
1 -> 1
n -> n * factorial (n-1)
end
なので、両方を合わせたこんな感じの関数定義の構文が使えるようにしました。
fun factorial : Int -> Int = {
1 -> 1
n -> n * factorial (n-1)
}
関数の値への適用(呼び出し)は Haskell ふうに
factorial 5
#=> 120
と、引数をカッコでくくらずに並べます。
多重ディスパッチと自由な演算子オーバーロード
もうひとつ欲しかったのが、自由な演算子オーバーロードでした。たとえば Ruby のように、 + のような演算子をいろいろな意味で気軽に定義したい。
1 + 2 は 3
"a" + "b" は "ab"
のように。
そこで、多重ディスパッチという仕組みを取り入れました。同じ関数名(演算子名)でも、引数の型が一つでも違えば違う関数として定義することができるというものです。
# 整数に1を足したものを返す
fun incr : Int -> Int = {
x -> x + 1
}
# 数の英語表現に1を足したものを返す
fun incr : String -> String = {
'zero' -> 'one'
'one' -> 'two'
'two' -> 'three'
}
# 同じ関数名でも引数の型によって異なる関数が呼び出される
incr 2 #=> 3
incr 'two' #=> 'three'
# . 演算子を使えばオブジェクト指向的なポリモーフィズムに見える
2.incr #=> 3
'two".incr #=> 'three'
上は、作っている言語での実際に動作するサンプルです。同じ関数名でも引数の型によって動作がちゃんと違っています。
この言語では、演算子も関数として定義されるようにしています。なので、多重ディスパッチの仕組みを使うことによって、同じ演算子を次のようにいろんな意味に使い分けることができます。
# 整数と整数の掛け算
2 * 3
#=> 6
# 文字列と整数の掛け算
"abc" * 3
#=> "abcabcabc"
# リストと整数の掛け算
[1,2,3] * 3
#=> [3,6,9]
# リストとリストの内積
[1,2,3] * [1,2,3]
#=> 14 (1*1 + 2*2 + 3*3)
# 関数合成
(incr * factorial) 5
#=> 121 (nの階乗に1を足す、という関数にn=5を適用する)
多重ディスパッチ(Multiple Dispatch )の仕組みはぼくにとってはかなり面白かったので、その頭文字を取って言語名を Mud としました。
なお、多重ディスパッチを機能として前面に出している言語としては、最近では Julia があると思います。
どうやって作っているか
実装言語は Haskell です。
megaparsec というライブラリで字句解析、構文解析をして式を抽象構文木に変換しています。その後に型検査をして、問題がなければ、抽象構文木のまま評価しています。つまり、コンパイラーではなくてインタープリターで、その中でも最も簡単な(速度が出ない)やり方です。 Tree Walking Interpreter というそうです。
ソース
https://github.com/mitsuchi/mud/
最初は42みたいな数字だけを認識できるものを作り、次は足し算だけができるものをつくり、つぎは変数が使えるようにする、というふうに機能を足しながら作りました。これだと、小さいながらどの段階でも言語としてちゃんと動くので、作っていて楽しいです。このあたりは次の本を参考にしました。
低レイヤを知りたい人のためのCコンパイラ作成入門(Rui Ueyamaさん)https://www.sigbus.info/compilerbook/
どこらへんが楽しいか
言語を作るたのしさは、好きな構文や機能を自分でデザインできちゃうところだと思います。空想地図を書いたりする人たちの楽しみと近いところがあるかもしれない。
こういう構文があったらなーとか、既存の言語のここがこうなっていて欲しいなーというときに、それを実現したものが(苦労するけど)作れて、なおかつ動くというのは嬉しい。
それから、どこまでもいじれてしまうというところも趣味に向いてます。たとえば言語の核の機能をもっと足そうとか、ライブラリーを足そうとか、エラー処理をまともにしようとか、いじろうと思えばずっといじれます。
この言語も、整数と変数が使えて関数が定義できるところまでできたらいいなと思っていました。が、いざ動くようになってくると、やっぱり浮動小数点数も使いたい、文字列も使いたい、高階関数も、多相性も、といろいろと欲がでて少しずつ大きくなっています。
つぎはコンパイラー型にしてみたいです。LLVMのIRを出力するようにできたら楽しいだろうなと思います。
動作デモ
ブラウザーから、この言語のいろんなサンプルコードを編集して動かせるページを作りました。よければ遊んでみてください。
Mud プレイグラウンド
http://mud.tiny-app.net/playground/