2023年なのだから、当然Common Lispを学ぶ
(log.schemescape.com)- 将来も使える言語を考える流れの中で、今回の選択は実用性よりも、古いがモダンな開発ワークフローを自分で実験してみることに近い
- JanetはLuaに近い領域を狙う小さなLisp/Clojure系スクリプト言語で、標準ライブラリとマクロベースのメタプログラミングによって関心を引いた
- Janetの長所に見えた移植性、埋め込み可能性、Parsing Expression Grammarsは現在の必要性と重なる部分が限られており、CHICKEN Scheme、CLISP、SBCL、Packratといった既存の選択肢でも十分に思えた
- Common LispのREPL主導開発は、ランタイムエラー状態でスタック、変数、ライブデータを調べて修正したあと、中断された実行を再開できる点が核心である
- 新しい言語と標準ライブラリを学ぶコストはあるが、このワークフローが最初から設計に組み込まれた環境を自分で体験してみる価値があると判断した
JanetからCommon Lispへ関心が移った理由
- 将来も使えるコードを書きたくて future-proof programming languages を考えてきたが、何かを作って共有するときには、人気があり、移植性が高く、便利な言語を選ぶという実用主義的な姿勢を取っている
- 一方で、楽しみや実験のために別のプログラミング言語やツールも見ている
- 最近、無料の本 Janet for Mortals に触れたことで Janet に興味を持った
- JanetはLuaに近いニッチを狙う、比較的小規模なLisp/Clojureに着想を得たスクリプト言語である
- 実用的な標準ライブラリ、Lisp的メタプログラミング、マクロによるコンパイル時実行を備えている
- Janetで最も惹かれたのは、一般的なLispらしさに加えて、移植性、埋め込み可能性、Parsing Expression Grammars だった
- 現時点では言語を埋め込む必要がないため、埋め込み可能性は決定的な理由にはならなかった
- 移植性については、CHICKEN Scheme、CLISP、Steel Bank Common Lisp も十分良さそうだった
- パースには Packrat が妥当な選択に思えた
- 結局、既存のLispではなくJanetを選ぶ理由は少なくなった
Common Lispを学ぶきっかけになったREPL主導ワークフロー
- Common Lispのほぼ独自ともいえるREPL主導ワークフローを扱った記事が学習の動機になった
- 例として示されるのは、
foo関数がまだ定義されていないbar関数を呼び出し、fooの実行中にエラーが発生する状況である- 古いLispやSmalltalk環境では、
fooの中断はbreakloopにつながる - breakloopは、中断された関数の動的環境の中にある完全なREPLである
- 中断された呼び出しスタックを行き来しながら、各スタックフレームで見える変数を確認できる
- 実行中プログラムのライブデータを検査し、修正できる
- 誤った変数やフィールド値が原因なら、値を変更してから中断された関数を再開できる
- REPL内で不足している
bar関数を定義し、fooを再開して妥当な結果を得ることもできる
- 古いLispやSmalltalk環境では、
- これまでもさまざまな “edit and continue” 機能は見てきたが、Common Lispがこうしたワークフローを念頭に置いて作られている点は新鮮だった
- これまでのデバッグ経験は、
printf debuggingから実際のデバッガ利用へと移ってきたprintf debuggingは、ログ出力コードを一時的に埋め込み、再コンパイルして、実行結果を確認する方式である- プログラムを修正したり再実行したりできない状況では、デバッガを学ぶ必要があった
- 本格的なデバッガは正しい道具だが、デバッグ環境の設定は苦痛だったり、ときには不可能だったりする
- Common Lispは、デバッグをさらに一段押し進めた環境のように見えた
- デバッガ内でメモリを書き換え、修正案を試した経験はあった
- しかし、実行中のプロセスの中でコードを書き直してパッチを当てられる点が特に魅力的だった
- Common Lispを学ぶ目的は、プログラムを対話的に構築しながら、このやり方が楽しい作業方法なのかを自分で確かめることにある
- 新しいワークフローを探るために新しい言語と標準ライブラリを学ぶ選択が最善かどうかは確信がないが、より良い代案もはっきりしない
- 他の言語にも似たようなREPLとエディタの統合があるかもしれない
- ただし、そうした機能は最初から支援されていたのではなく後付けの可能性があり、そのぶんバグが多いかもしれないと見ている
- 満足できなかったとしても、標準ツールで最も良い条件のもと試したという事実は残る
- Common Lisp学習に役立った資料は、別記事 Common Lisp resources にまとめられている
1件のコメント
Hacker News のコメント
これまで Common Lisp で良いと思った点は、コンディションシステムがすっきりしていて、restart によって離れた場所にあるコードも簡単に制御できることです。REPL 駆動開発も、何が起きるかよく分からない状況、たとえば馴染みのないデータソースをパースするときに、文脈を失わずにコードを直して続けられるので便利です。
実装が多く相互運用性も高いので、あるケースでは速度重視の SBCL とメモリ使用量を抑える CLISP を切り替えて使えました。Lisp を学ぶのに Scheme より CL 寄りになった理由の一つも、互換実装が複数あることです。
Emacs 初心者でも Common Lisp との統合は素晴らしく、開発に使っている非常に遅いネットブックでもよく動きます。最近の高速なコンピューター、VS Code、言語サーバーがある時代にはそこまで大きな利点ではありませんが、確かにレトロフューチャー感があります。
残念な点もあります。最も人気のあるパッケージマネージャー QuickLisp は悪くありませんが、現代的な言語エコシステムで慣れている機能水準には及びません。言語自体が時間の中で固定されているため、スレッド、同期、コマンドライン引数など多くのものに相互運用ライブラリが必要です。そして SBCL が glibc ではない Linux ディストリビューションにもバイナリを配布できるよう、完全な静的ビルドをサポートしてくれたら本当にうれしいです。
ソースコードを取得するという重要な作業で暗号化や認証をサポートしていないなら、セキュリティを気にする人にとって QL は選択肢になりにくいと思います。
さらに SBCL が SourceForge でホストされているのも問題です。SourceForge は過去にダウンロードアーカイブへマルウェアを注入したことがあるので、これもセキュリティ上の問題だと思います。今日 SourceForge を使い続ける妥当な理由は見当たらないのに、重要なプロジェクトがまだそこに残っている理由がよく分かりません。
Lisp、とりわけ Common Lisp は本当に好きですが、こうしたことのせいで離れてしまいましたし、直そうという関心もあまりなさそうに見えます。
https://www.timmons.dev/posts/static-executables-with-sbcl.h...
https://www.timmons.dev/posts/static-executables-with-sbcl-v...
https://cons.io
Gerbil/Gambit Scheme は CL の代替として、完全な静的バイナリを生成できます。
通常の Quicklisp ライブラリとしても、即座に起動するコアイメージとしても、バイナリとしても使えます。スクリプトもほぼ即座に実行できるので、Babashka に似た面があります。まだ細部を磨いているところで、v1.0 ではありません。
ブログの「ランタイムエラーを再起動なしで、壊れたコードをその場で直して処理する」はここで見せるべきでした: https://www.youtube.com/watch?v=jBBS4FeY7XM
長く重い計算を走らせて最後の段階でエラーが出たら、最初から再実行する代わりに対話型デバッガに入り、エラー行へ移動し、修正した関数をコンパイルしてからデバッガに戻り、スタックフレーム内の一点から実行を再開します。その後、プログラムが通るのを確認できます。
M-で REPL にコピーするやり方は、Clojure が提供するtap>よりずっと良いと感じます。Portal のようなツールを使っても、tap>は見栄えのよい pretty print に近いと思います。性能面でも Common Lisp は静的型指定ができ、SBCL はそうすると非常に効率的なコードを出せます。自分で書いたコードに
DISASSEMBLEをかけて実際に何が出力されるかを見て最適化するのが役に立ちます。特に SB-SIMD や Loopus のようなパッケージは、数値計算アプリケーションに本当に大きく役立ちます。:quackキーが true ならアヒルのように扱え」というやり方がうまく混ざっていて、エンティティ・コンポーネント・システム式のゲーム設計にはよく合います。しかし Common Lisp の開発スタイル、そして何よりコンディションシステムは、この1年ずっと本当に恋しかったです。CL にそれほど熟達したハッカーでもないのに、すぐに身体になじみました。CLOS と CL のプリミティブデータ型が、見た目以上にうまく噛み合っていたらよかったのですが。
Cider では確かに簡単にできますし、他のエディタの主要なツールも同じように十分うまくやれると思います。
DISASSEMBLEと似たことはある程度できます。https://github.com/Bronsa/tools.decompiler のような補助プロジェクトがあり、OpenJDK の JitWatch(https://github.com/AdoptOpenJDK/jitwatch)もある。他の JVM にも似たようなツールがある
Lisp ほど直感的ではないが、それでも可能ではある
魅力的にうまく動く何か難解なものを作り上げたのはよいが、今では年収を 0 の数で表現できる人たち以外、誰もそれに触りたがらないだろう
https://www.digitalocean.com/community/tutorials/java-repl-j...
https://github.com/waf/CSharpRepl
https://pub.dev/packages/interactive
Ruby、Python、PHP、Lua は言うまでもなく、C++ にも粗いながら REPL がある: https://github.com/root-project/cling
Lisp における REPL はもはや差別化要因ではないと言うたびにダウンボートされる
https://github.com/kaveh808/kons-9
ただし、こうした再定義や上書きは結局名前空間を汚染し、どこかの時点で何かが壊れてサーバーを再ロードしなければならなくなる
欠点も多いが、他のほとんどすべての言語や開発環境が持つ純粋に技術的な欠点より悪いわけではない。Python や JS のほうが広く使われ、ライブラリも多いが、開発体験ははるかに悪いと感じる。C#、TS、Py、Hs から、さらに難解な言語まで何年もかなり使ってきたが、それらの言語で行き詰まったり、明らかにひどいものに本気で腹が立ったりするたびに、CL(SBCL + Emacs + Slime) に戻ってくる。すると安心し、この世界にもまだ良いものがあるのだと確信できる
今は、私たちが愚かにも TypeScript でブートストラップした製品の資金調達を進めているところだが、リリース版は CL で作り直す予定だ。つまり今後リリースまでの 3〜5 年間は CL で、CL の中で作業することになり、DSL 実装、コード生成、マクロ作業、静的型解析器の実装といった楽しいことをやれるのでうれしい
C# で作業しているとき、Lisp の何が恋しくなる?
私の過激な考えでは、関数型プログラミングが広く流行らなかった理由は、プログラムを書くには確かに悪くないものの、今日のソフトウェアの大半は「自分の PC/サーバー上でコマンドラインから実行され、終了するまで動くプログラム」ではなく、「起動してユーザー入力に反応し、ユーザーが閉じるまで動くプログラム」か、「起動したあとネットワークや他の自動 I/O に応答し、別のソフトウェアに止めろと言われたら止まるプログラム」だからだ
こうした作業は純粋関数型スタイルで行うのがずっと難しい。少なくとも、私が使ったことのある主張の強い関数型実装ではそうだった。もはや単に何らかの式を評価するだけではなく、状態を初期化し、I/O に反応し、状態を更新したり追加の I/O を行ったりし、監視・受信・追加 I/O・状態更新のために並列化まで使うことがあるからだ
もちろん Lisp でこうしたことをするのが不可能なわけではないが、学部時代に数学期触れた関数型プログラミングと、C++/Scala で業務として関数型機能を使ってこうした問題を解いた経験からすると、この種のアプリケーションで関数型プログラミングをうまく機能させるのはかなり難しいと感じたし、その不適合さが関数型プログラミングにより深く踏み込むことをためらわせている
Common Lispとは別に、コードを生成するコードを書かなければならないことがよくあった。この領域ではLispが輝きそうだが、まだ試す機会はなかった。Common Lispを考える前に進めていた最近のプロジェクトの例としては、静的サイトジェネレーターで特定のJSON Schemaを検証するコード生成、プログラミングゲームのストーリーコンテンツに使うためにMarkdownからJSXを生成する作業がある
これまで書いてきた数え切れないほどのCマクロは言うまでもない
ほかのスタイルの優れた機能をすべて捨てる必要もない。関数型プログラミングの一部は、一般的な言語でもますます普通になっている。たとえばRustも多くの場合に関数型パターンを使う
そしてLispは望めばオブジェクト指向や命令型スタイルでも書ける。Haskellではない
Lispは出発点であり、Haskellが本物だと思う。実際にプロダクションコードを書かないとしても、そのスタイルを学ぶことを勧める
(let ((pair (cons 1 nil)))(setf (cdr pair) pair)(list (first pair) (second pair) (third pair)));; => (1 1 1)「永遠にコンパイルされるもの」が欲しければANSI Cで書く
「永遠に生き残るもの」が欲しければPython 2.7で書き、組織全体のインフラテンプレートの中核にする。カスタムAnsibleモジュールなら加点
自分が望んでいるのは、Lispが雑多なものやボイラープレートを最小化、または抽象化して、考えをコードで表現しやすくしてくれることだ。自分には、Lisp式が考えを表現する最も自然な方法のように見える
主にGUIアプリの作成に関心があるので、Clogを習得するか、何らかのGUIツールキット上の良いラッパーを見つける、あるいは開発したい
(do-it (do-it first))(do-it first)はうまく動くが、(do-it (do-it first))の呼び出しが失敗したらどうなるのか?壊れた地点で制御を得て、
do-itの定義を直せるならよい。しかし直すと、(do-it first)の結果も変わるすると、機械が現在いる地点は、現在のコードではもはや到達できない地点になる
続行できるようにする修正をした結果、その修正のために制御を得た状態そのものが変わる場合、これがどう動作するのか本当に分からない