3 ポイント 投稿者 GN⁺ 2023-07-28 | 1件のコメント | WhatsAppで共有
  • 将来も使える言語を考える流れの中で、今回の選択は実用性よりも、古いがモダンな開発ワークフローを自分で実験してみることに近い
  • JanetはLuaに近い領域を狙う小さなLisp/Clojure系スクリプト言語で、標準ライブラリとマクロベースのメタプログラミングによって関心を引いた
  • Janetの長所に見えた移植性、埋め込み可能性、Parsing Expression Grammarsは現在の必要性と重なる部分が限られており、CHICKEN Scheme、CLISP、SBCL、Packratといった既存の選択肢でも十分に思えた
  • Common LispのREPL主導開発は、ランタイムエラー状態でスタック、変数、ライブデータを調べて修正したあと、中断された実行を再開できる点が核心である
  • 新しい言語と標準ライブラリを学ぶコストはあるが、このワークフローが最初から設計に組み込まれた環境を自分で体験してみる価値があると判断した

JanetからCommon Lispへ関心が移った理由

  • 将来も使えるコードを書きたくて future-proof programming languages を考えてきたが、何かを作って共有するときには、人気があり、移植性が高く、便利な言語を選ぶという実用主義的な姿勢を取っている
  • 一方で、楽しみや実験のために別のプログラミング言語やツールも見ている
    • NimZigのような短い名前の興味深い言語を追っていた
    • こうした実験は、新しいプログラミングのやり方や、後に必須ツールになるものを見つけるきっかけにもなる
  • 最近、無料の本 Janet for Mortals に触れたことで Janet に興味を持った
    • JanetはLuaに近いニッチを狙う、比較的小規模なLisp/Clojureに着想を得たスクリプト言語である
    • 実用的な標準ライブラリ、Lisp的メタプログラミング、マクロによるコンパイル時実行を備えている
  • Janetで最も惹かれたのは、一般的なLispらしさに加えて、移植性、埋め込み可能性、Parsing Expression Grammars だった
    • 現時点では言語を埋め込む必要がないため、埋め込み可能性は決定的な理由にはならなかった
    • 移植性については、CHICKEN SchemeCLISPSteel Bank Common Lisp も十分良さそうだった
    • パースには Packrat が妥当な選択に思えた
  • 結局、既存のLispではなくJanetを選ぶ理由は少なくなった

Common Lispを学ぶきっかけになったREPL主導ワークフロー

  • Common Lispのほぼ独自ともいえるREPL主導ワークフローを扱った記事が学習の動機になった
  • 例として示されるのは、foo 関数がまだ定義されていない bar 関数を呼び出し、foo の実行中にエラーが発生する状況である
    • 古いLispやSmalltalk環境では、foo の中断はbreakloopにつながる
    • breakloopは、中断された関数の動的環境の中にある完全なREPLである
    • 中断された呼び出しスタックを行き来しながら、各スタックフレームで見える変数を確認できる
    • 実行中プログラムのライブデータを検査し、修正できる
    • 誤った変数やフィールド値が原因なら、値を変更してから中断された関数を再開できる
    • REPL内で不足している bar 関数を定義し、foo を再開して妥当な結果を得ることもできる
  • これまでもさまざまな “edit and continue” 機能は見てきたが、Common Lispがこうしたワークフローを念頭に置いて作られている点は新鮮だった
  • これまでのデバッグ経験は、printf debugging から実際のデバッガ利用へと移ってきた
    • printf debugging は、ログ出力コードを一時的に埋め込み、再コンパイルして、実行結果を確認する方式である
    • プログラムを修正したり再実行したりできない状況では、デバッガを学ぶ必要があった
    • 本格的なデバッガは正しい道具だが、デバッグ環境の設定は苦痛だったり、ときには不可能だったりする
  • Common Lispは、デバッグをさらに一段押し進めた環境のように見えた
    • デバッガ内でメモリを書き換え、修正案を試した経験はあった
    • しかし、実行中のプロセスの中でコードを書き直してパッチを当てられる点が特に魅力的だった
  • Common Lispを学ぶ目的は、プログラムを対話的に構築しながら、このやり方が楽しい作業方法なのかを自分で確かめることにある
  • 新しいワークフローを探るために新しい言語と標準ライブラリを学ぶ選択が最善かどうかは確信がないが、より良い代案もはっきりしない
    • 他の言語にも似たようなREPLとエディタの統合があるかもしれない
    • ただし、そうした機能は最初から支援されていたのではなく後付けの可能性があり、そのぶんバグが多いかもしれないと見ている
    • 満足できなかったとしても、標準ツールで最も良い条件のもと試したという事実は残る
  • Common Lisp学習に役立った資料は、別記事 Common Lisp resources にまとめられている

1件のコメント

 
GN⁺ 2023-07-28
Hacker News のコメント
  • ここで自分の記事を見ることになるとは思わなかった。いつか続編も書きたいけれど、まだ初心者です。
    これまで Common Lisp で良いと思った点は、コンディションシステムがすっきりしていて、restart によって離れた場所にあるコードも簡単に制御できることです。REPL 駆動開発も、何が起きるかよく分からない状況、たとえば馴染みのないデータソースをパースするときに、文脈を失わずにコードを直して続けられるので便利です。
    実装が多く相互運用性も高いので、あるケースでは速度重視の SBCL とメモリ使用量を抑える CLISP を切り替えて使えました。Lisp を学ぶのに Scheme より CL 寄りになった理由の一つも、互換実装が複数あることです。
    Emacs 初心者でも Common Lisp との統合は素晴らしく、開発に使っている非常に遅いネットブックでもよく動きます。最近の高速なコンピューター、VS Code、言語サーバーがある時代にはそこまで大きな利点ではありませんが、確かにレトロフューチャー感があります。
    残念な点もあります。最も人気のあるパッケージマネージャー QuickLisp は悪くありませんが、現代的な言語エコシステムで慣れている機能水準には及びません。言語自体が時間の中で固定されているため、スレッド、同期、コマンドライン引数など多くのものに相互運用ライブラリが必要です。そして SBCL が glibc ではない Linux ディストリビューションにもバイナリを配布できるよう、完全な静的ビルドをサポートしてくれたら本当にうれしいです。
    • 欠点もいくつかあります。最後に確認したとき、QuickLisp は暗号化も、転送中の改ざんを検出する検証メカニズムもなく、平文 HTTP だけでパッケージを取得していました。
      ソースコードを取得するという重要な作業で暗号化や認証をサポートしていないなら、セキュリティを気にする人にとって QL は選択肢になりにくいと思います。
      さらに SBCL が SourceForge でホストされているのも問題です。SourceForge は過去にダウンロードアーカイブへマルウェアを注入したことがあるので、これもセキュリティ上の問題だと思います。今日 SourceForge を使い続ける妥当な理由は見当たらないのに、重要なプロジェクトがまだそこに残っている理由がよく分かりません。
      Lisp、とりわけ Common Lisp は本当に好きですが、こうしたことのせいで離れてしまいましたし、直そうという関心もあまりなさそうに見えます。
    • 静的ビルドが必要で、少し古い SBCL を使ってもよいなら、musl 環境で SBCL をビルドしてリンクした daewok の作業が探していた解決策かもしれません。最新バージョンへパッチを移植しようとしましたが、upstream の変更のためセグメンテーションフォルトが残ります。
      https://www.timmons.dev/posts/static-executables-with-sbcl.h...
      https://www.timmons.dev/posts/static-executables-with-sbcl-v...
    • cons を一つあげます。
      https://cons.io
      Gerbil/Gambit Scheme は CL の代替として、完全な静的バイナリを生成できます。
    • 優れたライブラリ群(json、csv、データベース、HTTP クライアント、コマンドライン引数、言語拡張など)から始めたいときのために CIEL を作っています: https://github.com/ciel-lang/CIEL/
      通常の Quicklisp ライブラリとしても、即座に起動するコアイメージとしても、バイナリとしても使えます。スクリプトもほぼ即座に実行できるので、Babashka に似た面があります。まだ細部を磨いているところで、v1.0 ではありません。
      ブログの「ランタイムエラーを再起動なしで、壊れたコードをその場で直して処理する」はここで見せるべきでした: https://www.youtube.com/watch?v=jBBS4FeY7XM
      長く重い計算を走らせて最後の段階でエラーが出たら、最初から再実行する代わりに対話型デバッガに入り、エラー行へ移動し、修正した関数をコンパイルしてからデバッガに戻り、スタックフレーム内の一点から実行を再開します。その後、プログラムが通るのを確認できます。
  • 仕事では Clojure を使っていますが、デバッグするときや高速なコードが必要なときのたびに、Common Lisp のほぼすべてが恋しくなります。ネストしたエラーの中でスタックのどの部分でもクリックしてレキシカルバインディングを調べられるのは非常に有用で、オブジェクトをクリックしてから M- で REPL にコピーするやり方は、Clojure が提供する tap> よりずっと良いと感じます。Portal のようなツールを使っても、tap> は見栄えのよい pretty print に近いと思います。
    性能面でも Common Lisp は静的型指定ができ、SBCL はそうすると非常に効率的なコードを出せます。自分で書いたコードに DISASSEMBLE をかけて実際に何が出力されるかを見て最適化するのが役に立ちます。特に SB-SIMD や Loopus のようなパッケージは、数値計算アプリケーションに本当に大きく役立ちます。
    • Clojure で修士論文を書きながら感じた不満をうまく要約してくれています。選択そのものが不満というわけではありません。Clojure は「すべてがキー・バリューのマップ」という感覚と、「:quack キーが true ならアヒルのように扱え」というやり方がうまく混ざっていて、エンティティ・コンポーネント・システム式のゲーム設計にはよく合います。
      しかし Common Lisp の開発スタイル、そして何よりコンディションシステムは、この1年ずっと本当に恋しかったです。CL にそれほど熟達したハッカーでもないのに、すぐに身体になじみました。CLOS と CL のプリミティブデータ型が、見た目以上にうまく噛み合っていたらよかったのですが。
    • Clojure では flowstorm を見てみる価値があります。関数内を前後にステップ実行でき、その関数群と一緒にマップを REPL に送れます。
    • どちらの言語も、フォームを REPL に送る機能を言語自体として提供しているわけではなく、それはツールの機能だと思います。
      Cider では確かに簡単にできますし、他のエディタの主要なツールも同じように十分うまくやれると思います。
    • Clojure でも DISASSEMBLE と似たことはある程度できます。

https://github.com/Bronsa/tools.decompiler のような補助プロジェクトがあり、OpenJDK の JitWatch(https://github.com/AdoptOpenJDK/jitwatch)もある。他の JVM にも似たようなツールがある
Lisp ほど直感的ではないが、それでも可能ではある

  • Steel Bank Common Lisp は、収益を上げるソフトウェア会社を作れるようにしてくれた働き者だった。これなしでは、ここまで生産的にはなれなかったと思う。REPL 主導のワークフローは素晴らしいし、Lisp イメージは非常に堅牢で、性能も高い
    • 興味がある人のために、その会社がどこなのか共有してもらえる?
    • かっこよく見えるけど、今は Emacs に戻るには面倒くさすぎる。主に Python と JavaScript の開発には、ほぼデフォルト設定に近い VSCode を使っている。CTO という役割なので、フルタイムでコーディングしているわけではない
    • 技術スタックが少しでも重要だった時代は、ほとんど過ぎ去ったと思う
      魅力的にうまく動く何か難解なものを作り上げたのはよいが、今では年収を 0 の数で表現できる人たち以外、誰もそれに触りたがらないだろう
    • REPL 主導のワークフローが驚異的だと言うけれど、今では事実上すべての仮想マシン/インタプリタ言語に REPL があることを知らないのかと思う
      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 はもはや差別化要因ではないと言うたびにダウンボートされる
  • 3D グラフィックスに関心があるなら、見てみる価値のある Common Lisp のコードベースがある。単純で理解しやすい状態に保とうとした
    https://github.com/kaveh808/kons-9
    • YouTube で Kaveh の CL チュートリアル動画を探してみるといい。本当によくできている
  • Clojure 開発者としては、break loop と REPL 主導のワークフローは魅力的に見えるし、Clojure にも間違いなく役立ちそうだ。ブラウザの優れたデバッグツールを使う JS/TypeScript フロントエンドのコーディングに、より近づけられるかもしれない。残念ながら、現在の Clojure エコシステムにおけるツールとコミュニティ支援の状況はかなり貧弱に見える
    • Clojure でも、Web サーバーのようにコード階層の深いところにある呼び出しがリクエストごとに起きる場合なら、本物の REPL フローをある程度まねできる。サーバーを動かし続けたまま複数の関数を書き直して再ロードし、ブラウザから再リクエストできる
      ただし、こうした再定義や上書きは結局名前空間を汚染し、どこかの時点で何かが壊れてサーバーを再ロードしなければならなくなる
  • CL が好きで、別のことをしていると本当に恋しくなる。数十年前の Lisp 経験と比べると、「モダン」な言語では多くのことがあまりに苦痛で、笑えもしない
    欠点も多いが、他のほとんどすべての言語や開発環境が持つ純粋に技術的な欠点より悪いわけではない。Python や JS のほうが広く使われ、ライブラリも多いが、開発体験ははるかに悪いと感じる。C#、TS、Py、Hs から、さらに難解な言語まで何年もかなり使ってきたが、それらの言語で行き詰まったり、明らかにひどいものに本気で腹が立ったりするたびに、CL(SBCL + Emacs + Slime) に戻ってくる。すると安心し、この世界にもまだ良いものがあるのだと確信できる
    今は、私たちが愚かにも TypeScript でブートストラップした製品の資金調達を進めているところだが、リリース版は CL で作り直す予定だ。つまり今後リリースまでの 3〜5 年間は CL で、CL の中で作業することになり、DSL 実装、コード生成、マクロ作業、静的型解析器の実装といった楽しいことをやれるのでうれしい
    • cons のない Lisp なんてありえない
      C# で作業しているとき、Lisp の何が恋しくなる?
    • なぜ TypeScript はその製品に不向きなのか?
  • ブログと投稿者のここのコメントには「コーディング」の話が多いが、どんな種類のソフトウェアを作り、どんなユースケースを対象にしているのかはほとんど見えない
    私の過激な考えでは、関数型プログラミングが広く流行らなかった理由は、プログラムを書くには確かに悪くないものの、今日のソフトウェアの大半は「自分の PC/サーバー上でコマンドラインから実行され、終了するまで動くプログラム」ではなく、「起動してユーザー入力に反応し、ユーザーが閉じるまで動くプログラム」か、「起動したあとネットワークや他の自動 I/O に応答し、別のソフトウェアに止めろと言われたら止まるプログラム」だからだ
    こうした作業は純粋関数型スタイルで行うのがずっと難しい。少なくとも、私が使ったことのある主張の強い関数型実装ではそうだった。もはや単に何らかの式を評価するだけではなく、状態を初期化し、I/O に反応し、状態を更新したり追加の I/O を行ったりし、監視・受信・追加 I/O・状態更新のために並列化まで使うことがあるからだ
    もちろん Lisp でこうしたことをするのが不可能なわけではないが、学部時代に数学期触れた関数型プログラミングと、C++/Scala で業務として関数型機能を使ってこうした問題を解いた経験からすると、この種のアプリケーションで関数型プログラミングをうまく機能させるのはかなり難しいと感じたし、その不適合さが関数型プログラミングにより深く踏み込むことをためらわせている
    • 良い指摘だ。今のところは全部趣味にすぎない。Common Lisp で作ったものは、平凡な Battlesnake クライアントと、昨日をもって閉鎖されたマルチプレイヤーの単語並べ替えゲームくらいだ。どちらも Lisp で作ったことで大きな利点を得たわけではないが、その過程で多くを学んだし、それこそが核心だった

Common Lispとは別に、コードを生成するコードを書かなければならないことがよくあった。この領域ではLispが輝きそうだが、まだ試す機会はなかった。Common Lispを考える前に進めていた最近のプロジェクトの例としては、静的サイトジェネレーターで特定のJSON Schemaを検証するコード生成、プログラミングゲームのストーリーコンテンツに使うためにMarkdownからJSXを生成する作業がある
これまで書いてきた数え切れないほどのCマクロは言うまでもない

  • Common Lispには、今でも最も進んだオブジェクト指向システムの一つであるCLOSが含まれている。ほとんどのLispはHaskellのような関数型言語ではない
  • Elixirは関数型プログラミングを使い、Web開発やフォールトトレラントなシステムが必要なときに非常に優れている
    ほかのスタイルの優れた機能をすべて捨てる必要もない。関数型プログラミングの一部は、一般的な言語でもますます普通になっている。たとえばRustも多くの場合に関数型パターンを使う
    そしてLispは望めばオブジェクト指向や命令型スタイルでも書ける。Haskellではない
  • モナドとIOモナドを理解するのに少し時間がかかり、それらを安全に合成し実行順序を操作する方法を身につけるにはさらに時間がかかった。しかし今では型安全なアプリケーションを書けるし、関数型でない言語で仕事をするときもバグを減らせている。給料をもらって使っている言語はJavaだ
    Lispは出発点であり、Haskellが本物だと思う。実際にプロダクションコードを書かないとしても、そのスタイルを学ぶことを勧める
  • Common Lispは「純粋関数型」言語ではなく、あらゆるパラダイムをサポートする。こんな奇妙なことも許される
    (let ((pair (cons 1 nil)))
    (setf (cdr pair) pair)
    (list (first pair) (second pair) (third pair)))
    ;; => (1 1 1)
  • 「永遠に実行されるもの」が欲しければRustで書く
    「永遠にコンパイルされるもの」が欲しければANSI Cで書く
    「永遠に生き残るもの」が欲しければPython 2.7で書き、組織全体のインフラテンプレートの中核にする。カスタムAnsibleモジュールなら加点
  • 「2023年だから当然Common Lispを学んでいる」という言葉のように自分も学習中だが、まだ言及されている機能が有用だとは感じていない。慣れれば考えが変わるかもしれない
    自分が望んでいるのは、Lispが雑多なものやボイラープレートを最小化、または抽象化して、考えをコードで表現しやすくしてくれることだ。自分には、Lisp式が考えを表現する最も自然な方法のように見える
    主にGUIアプリの作成に関心があるので、Clogを習得するか、何らかのGUIツールキット上の良いラッパーを見つける、あるいは開発したい
  • 要点は、SchemeとJanetも素晴らしいが、筆者はより独立実行型の言語を望んでいるということだ。違いを生むのは、プログラムエラーが発生したときに開く完全なREPLであるbreakloopだ。スタックトレースでもデバッガでもなく、現在壊れている地点から直接作っていく方式である
    • 本当にすごそうに聞こえるのに、なぜCommon Lispは最も人気のある言語ではないのか? コードをほとんど書かない立場からの質問だ
    • Clojure開発者として興味深く聞こえるが、次の場合はどうなるのか気になる。Lispらしいコードではなく、理解できない点を示すための例だ
      (do-it (do-it first))
      (do-it first)はうまく動くが、(do-it (do-it first))の呼び出しが失敗したらどうなるのか?
      壊れた地点で制御を得て、do-itの定義を直せるならよい。しかし直すと、(do-it first)の結果も変わる
      すると、機械が現在いる地点は、現在のコードではもはや到達できない地点になる
      続行できるようにする修正をした結果、その修正のために制御を得た状態そのものが変わる場合、これがどう動作するのか本当に分からない
    • エラーが起きた地点で完全なREPLが開き、スタックトレースやデバッガではなく現在壊れている地点から直接作るという話を聞くと、小さなSmalltalkイメージを開いてみたくなる
    • これはGambit Schemeでも標準だ
    • こうしたものはプロダクションで悪性コードやセキュリティ脆弱性の攻撃経路にならないのだろうか?