HN 日本語サマリー

← 一覧へ戻る
プログラミング

Lispへの道:なぜLispなのか

A Road to Lisp: Why Lisp (scotto.me)

277 pointsby silcoon268 コメント

要約

この記事は、多くのプログラマーがLispのコードに初めて触れた際に抱く疑問に答えることから始まります。Lispの独特な構文や学習曲線に触れつつ、その強力な機能、特にマクロによる言語拡張性やコードとデータが同一視される「ホモイコニシティ」といった概念が、プログラマーの思考法を根本から変え、より柔軟で強力なプログラミングを可能にすることを解説しています。

全文翻訳

Lispへの道:なぜLispなのか 2026年7月9日 ほとんどのプログラマーが初めてLispのコードを見たときに抱く疑問は、間違いなく「これは一体何だ?」というものです。私も、その型破りな構文、無数の括弧、奇妙なインデント、そしてformatの最初の引数で標準出力に書き出すという発想に、同じ疑問を抱きました。 (defun flip-coin-for-real () (<= (random 100) 80)) (defun hello-lisp () (write-line "What is your name?") (let ((name (read-line)) (learn-it (if (flip-coin-for-real) "should" "should not"))) (format t "Hello, ~A.~%" name) (format t "The Oracle said... you ~A learn Lisp!~%" learn-it))) 括弧の多いコードを読むことに慣れてきた後、パッケージやシンボルをどのように使うか、新しいプロジェクトを作成してライブラリをインポートする方法、REPLの使い方、条件(conditions)や再開(restarts)の使い方を学ぶ必要がありました。そして最も重要なのは、アルゴリズムを構築する際に、新しい考え方に切り替える必要があったことです。 Lispの旅は、ほとんどの一般的な言語と比較して学習曲線が急です。しかし、それらの言語では決して得られないスキルを解き放つこともできます。 なぜなら、Lispでは他の言語では不可能なことができるからです。Lispは新しい可能性を切り開き、アルゴリズムが異なる形をとることを可能にし、プログラマーにさらなる力と柔軟性をもたらします。Paul Grahamは、より強力でない言語しか使ったことのないプログラマーがLispの力を理解するのが難しい理由を説明するために、「Blubパラドックス」という言葉を造りました。要するに、彼らはそれらの言語に何が欠けているのか、そしてLispがそれらに対して持つ利点を認識するために必要な概念を欠いているからです。 Louis Armstrongはジャズについて似たようなことを言いました。「ジャズとは何かを尋ねなければならないなら、あなたは決して知ることはないだろう。」 — Louis Armstrong。 この記事では、この特別な言語を学ぶ価値がある理由を示すいくつかの機能について説明します。簡単な道ではありませんし、その力を真に理解するには、自分でLispを使ってみる必要があります。そして、たとえ最終的にLispを使わなくても、プログラミング言語が何ができるかについての異なる視点を得ることができます。Lispは、コードを使って問題について考える方法を変えるため、あなたをより良いプログラマーにするでしょう。具体的には、他のプログラミング言語では不可能なアプローチを教えてくれます。あなたはLisp自体をプログラミングできるようになり、それによって使用する言語を解決したい問題に適応させることができます。Lispを使用すると、言語を問題に向かって成長させ、その言語でプログラムを書くことを学ぶことができます。 拡張性 Lispはそれ自体の中で拡張可能です。専門家(リスパーと呼ばれます)は、それを「プログラマブルなプログラミング言語」と一般的に呼んでいます。プログラムのためのコードを書くだけでなく、Lisp自体を拡張するコードを書くことができます。これはマクロ演算子のおかげで可能です。 C、Rust、Swiftのような言語でマクロを使ったことがあるなら、それと同じものを期待しないでください。それらのマクロは、主に定型コードを排除したり、繰り返しコードを生成したりする方法です。それ以上に、Lispのマクロは、言語自体の一部となる新しい構文を作成することを可能にします。それらはLispプログラマーにとって習得するのが最も難しい機能の1つなので、ここではそれらの使い方や仕組みを教えようとはしません。それらが何をするのかの味見だけをします。 例えば、Cプログラマーはwhile演算子を使って簡単なループを定義したいかもしれません。Common Lispはそれを提供しないので、プログラマーはマクロを書いて言語に追加できます。 (defmacro while (condition &body body) `(loop while ,condition do (progn ,@body))) 新しいwhileマクロは、条件が真である限り、一連の命令(body)を実行します。これは、複雑な繰り返しを書くために使用される(シンボルwhileも使用していることに注意してください)loopを使用します。prognは複数の式を取り、順に実行し、最後の結果を返す特殊なフォームです。 (loop while ... do (progn ...))と書く代わりに、新しいwhile演算子を使用することで、コードを短縮し、Cに似たものにすることができます。私たちは利用可能な言語を拡張しただけです。 (let ((counter 3)) (while (> counter 0) (print counter) (decf counter))) ;; 3 ;; 2 ;; 1 初心者には、これが本当に魔法である理由が分からないかもしれません。なぜなら、ユニークな構文を除けば、defmacroは最初のコード例で新しい関数を定義するために使用されたdefunと非常に似ているからです。両者の違いをよりよく理解するために、新しいwhileを関数として定義し、それを呼び出してみましょう。 (defun fake-while (condition body) (loop while condition do (funcall body))) (let ((counter 3)) (fake-while (> counter 0) (progn (print counter) (decf counter)))) loopは以前と同じマクロです。funcallは、この場合bodyである、受け取った関数を呼び出します。上記のコードをCommon Lisp REPLで実行してみてください。次のようなエラー条件を受け取ります。 The value 2 is not of type FUNCTION. これは、fake-whileの引数が即座に評価されるために発生します。(< x 3)はt(真)に評価されます。prognは関数なので、その引数は評価されます。(print counter)は3を出力し、(decf counter)は2を返します。prognは最後の式の値を返すため、bodyは2になります。したがって、fake-whileは最初の引数としてtを、2番目の引数として2を受け取ります。コードブロックではなく値です。条件は真なのでwhileに入り、次にfuncallはbodyを呼び出す関数として期待しますが、bodyは値2です。これにより、上記の条件が発生します。コンパイラによって実行されるコードは次のようになります。 (funcall 2) は条件を発生させます。 (loop while t do (funcall 2)) 前述のマクロwhileは、引数(conditionとbody)を評価せずにそのまま保持するため、必要になるまで評価されません。マクロが操作する変換をREPLで検査できます。この変換は展開と呼ばれます。macroexpandは、コードを展開するために使用できる特別なLispコマンドです。 CL-USER> (macroexpand-1 '(while (> counter 0) (print counter) (decf counter))) (LOOP WHILE (> COUNTER 0) DO (PROGN (PRINT COUNTER) (DECF COUNTER))) マクロ展開から返されたコードは、受け取られ、コンパイラによって実行されるコードです。今回はwhileマクロが引数を保持しており、それらがコードの残りの部分と一緒に実行されることがわかります。マクロは、関数とは異なり、引数を事前に評価しません。代わりに、それらを純粋なデータとして扱います。これは、Lispでは(ほぼ)すべてがリストで構成されているため可能です。それらは主要なデータ構造であり、コードを書くために使用されます。すべてがリストなのです。 プログラムは、一連のシンボリック式(s-exprと略される)で構成されています。式とは、値に評価されるものすべてを意味する数学的な用語です。シンボリックとは、式が値とシンボルを使用して作成されることを意味します。s-expressionは次の2つのうちのいずれかです。 アトム — データ単位(数値、文字列、シンボルなど) リスト — アトムまたは他のリストのコレクション ;; アトム 数値 "hello" ;; アトム 文字列 '(1 "y" :c) ;; アトム(数値、文字列、キーワード)のリスト (+ 1 2) ;; シンボル(プラス)と2つの数値で構成されるリスト LispはLISt Processingを意味するため、言語の目的は、プログラムを構成するリストで表現された一連の命令を処理することです。リストは主要なデータ構造としてもコードを書くためにも使用されるため、興味深い特性を抽出できます。Lispでは、コードとデータはリストを使用して表現されるため、コード・アズ・データとなります。この特性はホモイコニシティと呼ばれます。 CL-USER> (+ 1 2) 3 CL-USER> '(+ 1 2) (+ 1 2) 同じ内容の2つのリストを使用しました。唯一の違いは、2番目のリストの前に'を付けたことですが、出力は異なります。最初のリストはコードとして扱われます。評価され、特にシンボル+は引数の合計を実行する関数にマッピングされます。Lispはポーランド記法と呼ばれるものを使用します。つまり、リストの最初の要素が関数名で、それに続くものが引数です。したがって、print("hello")と書く代わりに、(print "hello")と書くことになり、関数が移動します。