ホモアイコニックな Python 言語
(aljamal.substack.com)- Lisp の ホモアイコニシティ は、コードとデータを同じ形で扱う性質であり、古典的な「Lisp in Lisp」を Python に移し替えると、このアイデアがより馴染みのある構文の中で現れる
- もともとの Lisp は、コード表現である M-expression とデータ表現である S-expression を併せ持っており、「Lisp in Lisp」は M-expression で S-expression Lisp を実装している
- Python 版では S-expression を Python リスト で表現し、M-expression は関数呼び出しと条件文へ移し替えることで、別個のパーサなしにインタプリタを構成する
- 最初のインタプリタは
atom,eq,car,cdr,cons,appendのようなリストの基本原始演算の上に成り立ち、lambdaをサポートするためにassoc,pairlis, 環境リストが追加される evalは式と環境を一緒に受け取り、変数バインディングを処理し、pairlisとassocを通じて 動的スコープ 方式で引数と値を結び付ける
Lisp が示したコードとデータの統合
- Lisp は 1960 年代初頭に John McCarthy が著した Lisp paper と Lisp 1.5 manual を通じて、数十年後にも有効な多くのアイデアを残した
- その中核の一つが ホモアイコニシティ である
- 一般的な言語では、コードはデータに作用する演算の列として理解される
- Lisp はコードとデータを同じ形で扱い、演算子と被演算子の境界を曖昧にする
- Alan Kay は Lisp 1.5 manual 13 ページ下部の「Lisp in Lisp」コードを “Maxwell's Equations of Software” と呼んだ
- 数行のコードの中にプログラミング世界全体が宿っているという大きな気づきだった、という趣旨の引用が含まれている
「Lisp in Lisp」を Python に移し替える方法
- 目標は、古典的な「Lisp in Lisp」コードを Python で書き直しつつ、元のコードの精神をできるだけ維持することにある
- Lisp には 2 つの構文表現がある
- M-expression: コード表現、meta の略
- S-expression: データ表現、symbolic の略
- 両者は意味的には等価である
- 既存の「Lisp in Lisp」コードは M-expression で書かれ、S-expression Lisp を実装している
- Python 実装では Lisp の S-expression を Python リスト で表現する
- Lisp は “List Processing” の略であり、リストという単一のデータ構造を中心に動作する
- Python リストは Lisp の S-expression をエミュレートするのに適した表現として使われる
- M-expression は Python の関数呼び出しや条件文のようなコード構造へ翻訳される
- この対応付けのおかげで、文字列操作や別個の パーサ 実装なしにインタプリタを作れる
リストの原始演算で作る最初のインタプリタ
- Lisp 実装には、言語の外側から提供されるいくつかの 基本関数 が必要である
- Python 実装で使われるリストの原始演算は次のとおり
atom(x):xがリストかどうかを確認eq(x,y):xとyが等しいかを確認car(x): リストの先頭要素cdr(x): リストの残りcons(x,y): 原子をリストに付け加えるappend(x,y): 2 つのリストを連結する
- いくつかの再帰的な原始演算を除けば、Groq 上の Llama3-70b を活用して、「Lisp in Lisp」コードの部分集合を実行するインタプリタを素早く作ることができた
- 例では Python リストが S-expression のように動作する
- 全コードは github gists で公開されている
lambda と再帰のための拡張
- 最初の実装には重要な機能である
lambdaが欠けているlambdaは Lisp で匿名関数を定義し呼び出す主要な方法である- Lisp では
lambdaがなければ再帰を実装できない - 再帰がなければ、計算可能なあらゆるものを計算できる最小基準である チューリング完全性 に到達できない
lambdaを導入するためにassoc(x,y)とpairlis(x,y)が追加されるassoc(x,y)はリストで実装されたキー/値検索であり、連想リストを使うpairlis(x,y)は Python のzip(x,y)のように 2 つのリストを組にする
- 元の Lisp では、単純な線形走査であっても 再帰 で処理しなければならなかった
- 元の Lisp にはループがなかったためである
- Python への翻訳では
assocとpairlisをリスト内包表記でより簡潔に表現できる
COND処理では元の Lisp のevconをループに翻訳し、LAMBDA処理でもevlisに同じ方法を適用している
環境リストと動的スコープ
- 元の Lisp の
eval関数は 2 つの引数を受け取る- 1 つ目の引数は評価する S-expression
- 2 つ目の引数はキー/値の一覧からなる環境リストである
- 環境は
LAMBDA処理で 変数バインディング を保持する- 関数に
x変数があり、そこへデータを代入すると、pairlisがxシンボルとデータを結び付ける - 結び付けられた値は環境リストに保存または追加される
xが必要になるとassocが環境から探し出し、式に再び代入する
- 関数に
- このバインディング方式は 動的スコープ と呼ばれる
- 最終実装は元の「Lisp in Lisp」を Python に移し替えた形であり、最後の例では
lambdaの実行まで含まれている
2件のコメント
Hacker News のコメント
シンプルで高速な Python 風スクリプティング Lisp に興味があるなら、Clojure スタイルの言語として Hy と Janet は見る価値がある
起動の速いバイナリとして提供され、日常的な作業向けのライブラリも含まれている
CL↔Python 方面は https://github.com/CodyReichert/awesome-cl?tab=readme-ov-fil... も参考になる
自分は Hy のメンテナー だが、意味のある性能差が見えるならそれはバグだ
M 式はこの落とし穴を避けているので、Lisp の同図像性(homoiconicity)と概念的な優雅さを保ちながら、こうした括弧を要求しない実用言語があったのか気になる
まさに思い浮かべているそれ。説明も非常に直截的だ: “Program in YAML”
ほとんどこれ以上言うことはない
ちょっと待って、これは単に Python で実装した Lisp ではないのか?
タイトルが示唆するような同図像的な Python ではないように見えるが、自分は何を見落としているのだろう?
そのためには Python の文法がタプル、リスト、辞書で構成され、インタプリタがそれらを直接評価する必要があるだろう
Python で簡潔に実装できる別の関数型言語として Binary Lambda Calculus がある
コードのかなりの部分は BLC の純粋な入出力モデルを扱っており、変数検索には連想リストではなく de Bruijn インデックスで環境配列をインデックスする
同じページには他の 9 言語での実装もあり、BLC の自己インタプリタがパーサとトークナイザまで含めて 232 ビット(29 バイト)で最も簡潔だ
[1] https://rosettacode.org/wiki/Universal_Lambda_Machine#Python
MIT のプログラミング基礎科目では、全学生に Python で Lisp インタプリタ を書かせる
以前その授業が実際に Lisp で行われていた時代の名残だ
https://py.mit.edu/spring24
似たようなことを JS のリストでやったことがある: https://github.com/andrelaszlo/js-lisp
現代の言語が何十年も経ってから Lisp の驚くべき機能を再発見しているのは、笑えると同時に皮肉でもある
数日前、自宅サーバーの API を 9 時間呼び出していた Python プログラムが止まってしまい、腹が立った。似たような API 呼び出しで、事前定義したプロンプトテンプレートと文法制約を使って LLM を呼び出していた
残りの反復を別で回す前にプログラムの状態を保存して終了したかったが、実行中の Python コードを修正したり変数を覗いたりする方法が見つからず、結局 9 時間分の作業を失った
数日後に https://malisper.me/debugging-lisp-part-1-recompilation/ を見て、Common Lisp がこうした機能を何十年も前から言語に組み込んでいたことに驚いた。最強クラスのマクロシステムのような他の機能も同じ
だが、それこそが問題でもある。表現力が強すぎる
Lisp はいつもミクストメディアの視覚芸術を思い起こさせる。表現の自由は良さそうに見えるが、結局はより伝統的な単一メディアの芸術より成果物が概して劣ることが多い。媒体の制約も表現力と同じくらい重要だということが分かる
あるいは https://github.com/malor/cpython-lldb もある
さらに多くの方法は https://github.com/albertz/pydbattach/
この実装のどこが 同像的な Python なの?
プログラムが大きくなるにつれて Lisp が読みにくくなりがちな傾向を、ある程度抑えてくれる 型システム を持つ Lisp はあるのだろうか?
型システムでなくても、そうした性質を制御できる別の要素があるのか気になる
メタプログラミングは本当に素晴らしいが、時にはこれまで見た中で最も難解な抽象 Haskell のように読めるのに、道しるべになる型シグネチャすらない
型システムとリンターはコードを自動的に手なずける最良の道具だと思っているが、Lisp を選ぶ理由そのものを制約せずに Lisp プロジェクトの傾向をどう抑えられるのか、あまり想像できない
事実上の標準に最も近い SBCL は、コンパイル時の型チェックもかなり優れている
例えば
(declare (type String a b))の後に(+ a b)と書くと、Aの派生型がSTRINGなのにNUMBERであるべきだ、という警告を出してくれる私が Scheme から CL に移った最大の理由がこの型チェックで、その後は restarts や continuable asserts のようなちょっとした良い機能のおかげで居続けた
式指向の Python があれば、今の Python よりずっと良いものになるはず
この記事はタイトルが示唆する内容ではないが、Lisp の説明としては良い
それでも構わないが、「式を二級の構文として扱うこと」はかなり根本的で意図的な選択だ。括弧まみれを書こうとする開発者に苦労させるための設計でもある
だから
lambda:と:=は使用を抑えるためにわざと不格好で、匿名関数も書けないPython は、賢いことをしたいなら callable オブジェクトではなく イテレータ でやれ、と言う言語だ
すべてに Lisp 式の無文法 があればいいのに
私にとってはそのほうが良いし、幸い CL ではそうするのが簡単だ
Pythonに埋め込まれたLispの方言
https://hylang.org/