F#でMAL(Make a Lisp)を実装し始めた

MAL with F

少し前に1週間ほど休めたので前々から興味のあったMAL(Make a Lisp)LISPインタプリタ)を前々から興味のあったF#で実装しはじめました。連休中になんとかStep4までは完了させた(テストでOptionalになっているものは無視)ので、途中で思ったことのメモ。

eshamster/mal · GitHub(F#の実装はすでにあったので、esh-fsharpというフォルダ下で作成)

純粋にF#に関すること

  • 参考になったサイト:F#は基本的な文法に関したことを検索してみても、どうにも帯に短し襷に長しな情報(省略されているっぽいそこの情報を知りたいんですが…)や信頼性の低い情報(動かない!動かないよ!)が多く詰まることが多かったです。例えば、たかがクラスの継承に関する情報一つ調べるのに苦労するとは思いませんでした。
    • Home | F# for fun and profit:各項目がだいぶ網羅的に書かれていて信頼性も高く参考になりました。
    • F# - 七誌の開発Wiki:クラスに関する情報が網羅的に書かれていて参考になりました。
    • MSDN:帯に短い情報代表。本当に基本的なことを調べるには役に立ったものの、(F#の)例が少ない、情報が細切れすぎて使いづらいといった感じでした。
    • 安定のstackoverflow:襷に長い情報代表。なんだかんだで時々役立ちました。
  • 静的型解決について
    • 関数型ベースのマルチパラダイム言語、ということでLispに近いイメージで書き始めましたが、大間違いでした。具体的には、型なんて書かなくてもよしなに解釈してくれるんでしょー、まずはパーッと書いて後で合わせればいいよねー、と思っていたらすぐに詰まりました。結局、基本的に型は書いておかないとダメだという結論になりました。それまでは、型推論さんがつけてくれた型が自分のイメージと食い違い、深い階層で型エラー(コンパイルエラー)が頻発していました。
      • Haskell本でも型は書くのが基本と読んだ気がしますが、そちらのイメージで書かないといけなかったわけですね。
      • C#では型を省略できる(var)ところは基本省略で困らなかったのでそのイメージもあったのですが、F#は型推論が強すぎるために逆に頼りすぎると見えなくなるという感じでしょうか。
    • 久々に静的型言語をがっつり使いましたが、割りと嫌いになれないようです。面倒に思う部分も多々ある一方で、コンパイルさえ通れば大体安心というのはありがたい感覚でした。

気に入ったところ

  • match...with素敵
    • 大変素敵
    • とても柔軟かつ直感的で書きやすいswitch文といったところでしょうか。
    • 合わせてOptional Typeもいいですね。
  • 使わない引数を"_"とアンダーバー一つで表現できるのは楽でした。
    • CLだととりあえず受け取ってから(declare (ignore a b))ですし…。

気に入らなかったところ

  • インデントルール微妙
    • ネストを深くするのが面倒とか、一行が長くなった時に改行入れるのが毎回おっかなびっくりだとか。この辺りは、Emacsにfsharp-mode入れただけという手抜き環境も悪いので、Visual Studio等で使えば多少意見が変わるかもしれません。そうはいっても、文法解析だけでは一意にインデントが決まらない場合(結構多い)はどうしているのかと思いました。
  • 記号の意味を覚えるのが面倒
    • 段々と分かってはくるのですが、文脈によって突然裏切られることが多い気が…。特にカッコの要・不要・任意の区別がいまだにつかないです。結局は各記号の意味を深く理解しないといけないのかなと。(Common)Lispの場合だと、記号は丸カッコだけ、そのカッコにもどういう木構造を選択したか以上の意味はなく、その結果なのか文脈非依存で気楽だったなあと思いました*1
  • オブジェクト指向と関数型と入り交じっていて少し気持ち悪い
    • チェインを連ねるときにドットと関数呼び出しが当たり前のように入り混じるので、覚えにくくまた気持ち悪かったです。
  • コンパイラ重すぎません…?
    • 環境が貧弱なせいもあると思いますがそれにしても…。
    • Visual Studio + C#のときは、さほど重い印象はなかったのですが。

大変気に入っている部分と、気持ち悪く感じる部分とがあって、気に入るかどうか評価が難しいという印象です。

MALに関すること

  • F# + MALならUbuntuが無難
    • 久々にVisual Studioかと意気揚々と2015を入れたのですが、テストでmakeを使うのでまあLinuxですね
    • 普段の開発環境がCentOS(@さくらVPS)なので簡単に入れられないか調べていたのですが、どうも面倒くさそうという結論になりました。結局、2年ほど放置していたローカルのUbuntu仮想環境 on VirtualBoxを引っ張りだして使いました。
  • 第一級関数:Step2のプラスやマイナスなどの基本的な演算子を環境に登録するパートで思ったこと。
    • なんとなく分かったような分からないようなであった「関数が第一級オブジェクト」。静的型言語で実装すると文字通り関数型を用意しろということになるんですね。
      • C#で使えるdynamic使えないのだろうか、などと迷走の末にようやくそこに気づきました。MAL用の型システムを作っているんだという意識が希薄だったかなと反省。
      • (容易には)動的型を使えないことを不満に思ったりもしましたが、結果的には勉強になりました。
  • クロージャ:Step4にて
    • 上に同じく、なんとなく使えてるけど実はよく分かっていなかった代物。
    • 「引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。」(by Wikipedia)…なんだか分かるような分からないような…ですが、実装としては関数オブジェクト作成時にその時点の環境オブジェクトを渡しておくだけなんですね。文字通り関数が環境を覚えているという。
      • まあF#自身がクロージャを持っているようなので、それを使えばさらに容易に実装できるのでしょうが…。それはそれで調べる必要がありますが、勉強にはなりました。

今後

だいぶペースは落ちそうですが、思いの外勉強になるので徐々に進めていく予定です。ただ次のStep5の末尾再帰最適化は再帰で書いてきたものをループに書きなおすということで、関数型っぽさが失われそうで気が進まず手が止まっていますが*2チュートリアル自体は見ていて、末尾再帰は簡単にループにできるとよく言うけどなるほどなー、と感心はしているのですが。

*1:リードマクロで「便利な文法」を書くと文脈依存性が出てきてしまいますが…

*2:またしばらくCommon Lispで遊びたいのでということもあります