かんたんな自作言語のコンパイラをいろんな言語で書いてみるシリーズ 18番目の言語は OCaml です。 いつもの通りでライフゲームのコンパイルだけ通ればヨシ、という程度の雑なものです。
できたもの
主な部分のサイズ(行数)はこんな感じ。
$ wc -l *.ml lib/{utils,types}.ml 401 codegen.ml 127 lexer.ml 369 parser.ml 52 lib/utils.ml 6 lib/types.ml 955 合計
Ruby 版と同じくらい。
移植元
ライフゲームのプログラムだけコンパイルできればOKという簡単なコンパイラです。Ruby 版だとコンパイラ部分だけで 1000行くらい。
ベースになっているバージョンは ステップ 58 のあたり。 (追記 2023-04-15 ステップ 63 まで反映させました)
作り方はここに全部書いています(Ruby 版のものですが): vm2gol v2 製作メモ
動かし方の例
$ echo ' func add(a, b) { return a + b; } func main() { call add(1, 2); } ' | bin/lexer | bin/parser | bin/codegen # ↓アセンブリが出力される call main exit label add push bp cp sp bp cp [bp:2] reg_a push reg_a cp [bp:3] reg_a push reg_a pop reg_b pop reg_a add_ab cp bp sp pop bp ret label main push bp cp sp bp cp 2 reg_a push reg_a cp 1 reg_a push reg_a _cmt call~~add call add add_sp 2 cp bp sp pop bp ret
メモ
- Lisp が静的型になったみたいな印象(雑な印象)
- そんなに間違ってないっぽい:
OCamlでJavaScriptが幸せLisp + 型 → ML
- 加えて、型推論とパターンマッチとヴァリアントと……といった要素があるという感じ
- そんなに間違ってないっぽい:
- Haskell よりはとっつきやすい
- ref は使わなくても書けた
- コード生成で関数の引数名のリスト、ローカル変数名のリストに加えて label id も引き回す作りにしているので、ここはレコードでまとめてみた
- 多少読めるようになってきたので次は Haskell に再チャレンジしたい
- せっかくなので ocamlyacc を使えるようにしたい。
そして MinCaml を読みたい
- 今回は ocamlyacc などのライブラリは使っておらず、パーサは手書き
(以下、初心者が書いてますので不正確な記述・間違いなどあるかと思います)
書いていたら意外とすぐ慣れたけど最初よく分からなかった関数適用まわりの書き方について。
まず、1引数の関数しかないというのが前提。 引数なしの関数というのもない(たぶん)。 返り値も必ず1つ(たぶん)。
参考: なぜ関数型言語の関数は必ず値を返すのか - Qiita
引数なしの場合は ()
を渡す(()
に対して関数を適用する)
(* 関数の定義 *) let f () = print_string "hello\n" in (* 関数の適用 *) f ()
これは「関数名に続けて (
)
を書く」という構文なのではなくて、「()
という値(unit 型の値、ユニット値)に対して関数 f を適用している」と読む(『プログラミング in OCaml』 p152, 153)。
引数が2個以上の場合は、カリー化関数を使うか、タプルを使う。 (カリー化関数を使うといっても単に下記のように書くだけなので意識することはほとんどありませんでした)
(* カリー化 *) let add1 a b = a + b in add1 1 2 (* タプル *) let add2 (a, b) = a + b in add2 (1, 2)
使い分けについては
引数が組であることに意味がなければ,カリー化して定義することが推奨されます。 しかも,大抵の場合,カリー化関数のほうが効率良く実行できるようになっています。
(『プログラミング in OCaml』 p62)
とのことです。
ALGOL系の言語だと、関数呼び出しはこう。見慣れた感じ。
foo_func(1, 2)
Ruby では括弧を省略してこう書ける。
foo_func 1, 2
OCaml では引数の区切りのカンマは不要(スペースで区切る)。
foo_func 1 2
次のように括弧で囲んでいる場合、これは一見すると Lisp の関数適用と同じ見た目になっているけど、
中身の foo_func 1 2
の部分が式で、それを括弧で囲んでいる、と読むっぽい。
ALGOL系言語で 1 + 2
を括弧で囲んで (1 + 2)
にするのと同じような感覚。
(foo_func 1 2)
↑この例だと括弧を付ける必要はないが、評価結果に対してさらに別の関数を適用する、といった場合は括弧が必要になる(ないと区切りが分からない)。
bar_func (foo_func 1 2)
ここらへんの括弧の付け方が分かってくると普通に読み書きできるようになります。
if 式の then/else で複数の式を間違った形で書くとうまく動かないので、 慣れるまではとりあえず全部括弧で囲んでおけばよい(という感じでやってました)。 慣れてきて、ここは外しても大丈夫そうだな、と判断できるようになってきたら外していく。
if ... then ( ... ) else ( ... )
match も同様。
match ... with | 0 -> ( ... ) | _ -> ( ... )
ちなみに、私はめんどくさくて括弧で書いていましたが
文の並びを囲むときには括弧ではなくて begin/end を使うのが 「よいスタイル」 だと言われて推奨されています。
『プログラミング in OCaml』 p167
とのことです。
追記 2023-04-15
その後『プログラミングの基礎』も読みました。こういうタイトルですが OCaml の入門書でもあります。説明がとても平易で、一番最初に読む OCaml の入門書としておすすめ。
参考: [OCaml]書評「プログラミングの基礎」 - あどけない話
タイトルに OCaml と書かれていませんがこれも OCaml の本です。こちらも後から読みました(といっても図書館で借りてまだざっと目を通した程度)。扱われている内容の難易度的には今の自分にちょうどよさそうという印象で、購入しておこうかと。 ocamllex と ocamlyacc を使い、ターゲットは x64。