1. Lisp の quote を read と eval の二段階から読み解く
quote を最初に見たとき、「数値リテラルのリスト版」くらいに思う人は多いかもしれません。'(1 2 3) は [1, 2, 3] に似ている、くらいの感覚です。
しかし、quote はもっと根本的な何かをしているようにも感じます。
それは、「コードの評価」においてです。quote は Common Lisp において特殊演算子、つまり通常の関数のようには引数を先に評価せず、言語組み込みの評価規則を持つ形式です1。
1.1. quote は read と eval の間で止まる
(quote ...)、あるいは ' は、「引用」をします。
'xCode language: Lisp (lisp)
' は、reader が認識する省略記法で、(quote x) に展開されます2。
その後、eval が (quote x) を処理します。quote は特殊演算子なので、引数 x を評価しません。
シンボル X オブジェクトをそのまま返します。
つまり、Common LispのS式内のシンボルは、デフォルトではスロットの値が参照されるものの、quoteを使うと参照を抑制することができるのです。
値ではなく「それを指し示すものをそのまま言及する」という意味では、C言語での &x で変数のアドレスを取得するのに近いとも言えます。
「引用」によって変数を「メタ」的に、扱うことができるのです。
2. Lisp の評価は二段階ある
Common Lisp でコードが実行されるまでの流れは、大きく二段階に分かれます。
ソース文字列 → read → S式オブジェクト → eval → 値Code language: JavaScript (javascript)
- read(読み取り)は、テキストを Lisp のデータ構造に変換する段階です。
数値、文字列、シンボル、リスト……といったオブジェクトがここで生まれます。 - eval(評価)は、その S式オブジェクトを実行して値にする段階です。
シンボルなら変数の値を探し、リストなら関数呼び出しとして計算します。
この二段階という構造は、McCarthy が 1960 年の論文でS式(記号式)を扱う体系として Lisp を設計した時点から基本的な骨格となっています3。
2.1. "" は read の中で止まる
quoteに似たものとして、文字列オブジェクトを作る二重引用符("": double quotation)を考えることができます。
"hello"Code language: Lisp (lisp)
reader は " を見ると、モードが切り替わります。
次の " まで、中身を Lisp の構文として解析せず、そのまま文字列オブジェクトを作って返します4。
"hello" と書けば、reader の段階で「これは文字列データ」と決まり、eval が解釈する余地はありません。
つまり、"" は、read の中でコードとデータの境界を引く記法です。
2.2. quoteは構造を読み取る
x ; => x に束縛された値(たとえば 10)
'x ; => シンボル X そのものCode language: Lisp (lisp)
リストも同じです。
(+ 1 2) ; => 3
'(+ 1 2) ; => リスト (+ 1 2) そのものCode language: Lisp (lisp)
"" との違いは、「構造」の読み取りです。"(+ 1 2)" が文字列で文字の並びとして持っていて、構造として操作したければ、自分でパースしなければなりません。
一方、'(+ 1 2) はリストで、+ というシンボル、1 という数値、2 という数値が要素として入っています。car で先頭を取れるし、cdr で残りを取れます。
(car '(+ 1 2)) ; => +
(cdr '(+ 1 2)) ; => (1 2)Code language: Lisp (lisp)
つまり、文字列は文字の連続を保存しますが、quote はリストとシンボルの木構造を保存します。
ちなみに、quote されたリテラルオブジェクトを破壊的に変更した場合の結果は仕様上「未定義」とされているため、'(a b c) のようなリストを直接書き換えることは避けるべきです5。
2.3. 補足:#q() という発想
' が (quote ...) の省略記法なら、文字列にも (quote-string ...) のような記法があってよいのではないか、という考えました。
しかし、これはマクロでは書けず、リーダーマクロを使う必要があります。
#q(a (b 1a:)) ; => "a (b 1a:)"Code language: Lisp (lisp)
#q に対して set-dispatch-macro-character を設定し、次の ( から対応する ) までを文字列として読む関数を定義します6。
(defun read-q-paren-string (stream sub-char arg)
(declare (ignore sub-char arg))
(let ((depth 1))
(read-char stream) ; opening ( を読み捨てる
(with-output-to-string (out)
(loop for ch = (read-char stream nil nil)
while ch
do (cond
((char= ch #\() (incf depth) (write-char ch out))
((char= ch #\)) (decf depth)
(when (zerop depth) (return))
(write-char ch out))
(t (write-char ch out)))))))
(set-dispatch-macro-character #\# #\q #'read-q-paren-string)Code language: Lisp (lisp)
#q(...) の中身は S式として読まれていません。1a: のような通常の reader では弾かれるトークンも入れられます。
対応関係で見ると、こうなります。
'(...)は構造を保つ引用。
reader が S式を読み、eval がそれを評価しない。#q(...)は表面を保つ引用。
reader がそもそも S式として読まず、文字列にする。
#q は quote より一段手前、reader 自体へのブレーキです。
3. identity は eval の後で止まる
""は、コードを文字列として「そのまま返し」、quoteは、コードをリストとして「そのまま返し」ます。
その意味で、「引数をそのまま返す」恒等関数と言えます。
しかし、Common Lispには「引数の値をそのまま返す」identityという恒等関数があります7。。
(identity x)Code language: Lisp (lisp)
identity は呼ばれる前に、x はすでに「評価」されています。x に 10 が入っていれば、identity が受け取るのは 10 です。
; x = 10 のとき
(identity x) ; => 10
(quote x) ; => シンボル XCode language: Lisp (lisp)
identity は「値をそのまま返す」。quote は「S式をそのまま返す」。
「そのまま」の止まる場所が違うわけです。
3.1. 三つを並べると見えるもの
| 記法 | 止まる場所 | 何を保持するか |
|---|---|---|
"" | read の内部 | 文字列オブジェクト |
' (quote) | read と eval の間 | S式オブジェクト(構造) |
identity | eval の後 | 評価済みの値 |
どれも「何かをそのまま返す」という意味では恒等写像に近いですが、それぞれが扱うのは評価の流れの異なる段階にあるものです。
文字列インジェクションや SQL インジェクションの問題も、この図から見えてきます。
文字列としてコードを持つということは、"" の段階でコードを凍結していることになります。
しかし eval に渡る瞬間、文字列は再びパースされ、構造として読まれます。
その隙間に、攻撃者がコードを滑り込ませます。
Lisp のマクロが文字列生成ではなく構造生成として機能するのは、quote が文字列ではなく S式を保持するからです。
コードとデータの境界が、文字列の中ではなく構造の中に引かれています。
この性質は homoiconicity(同像性)と呼ばれ、Lisp がコードをデータ構造として自然に扱える理由として知られています8。
4. まとめ
quote を「リストリテラルを書く記法」として覚えることはできます。
実用上はそれで十分なことも多いです。
"" や identity と並べてみると、quote がどこに立っているかが見えてきます。
read と eval の二段階があって、quote はその間に位置する。
文字列よりも構造に近く、値よりも表現に近い場所で、S式を止めています。
Lisp でコードをデータとして操作できるのは、この「止まる場所」が文字列よりも奥にあるからです。
quoteが特殊演算子であることは Common Lisp の公式仕様に明記されています。仕様では「quoteはオブジェクトをそのまま返す」と定義されています。 – CLHS: Special Operator QUOTE- HyperSpec Section 2.4.3 では、シングルクォートに続く式
expが Lisp reader によって(quote exp)と同一に解析される、と定義されています。 – CLHS: Section 2.4.3 Single-Quote - Lisp の原点は John McCarthy が 1960 年に発表した論文です。この論文で S式を扱う体系としての LISP が定義され、read と eval に相当する処理の二段階が基盤として示されました。 – Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I
- Common Lisp の reader における文字列リテラルの扱いは仕様の Section 2.4.8 で規定されています。
"はダブルクォート文字に対応するリーダーマクロで、次の"まで文字を読んで文字列オブジェクトを構成します。 – CLHS: Section 2.4.8 Double-Quote - HyperSpec の
quoteの項では、リテラルオブジェクト(quoted objects を含む)を破壊的に変更した場合の結果は未定義と明記されています。作業用のリストが必要ならcopy-listでコピーしてから扱うのが基本です。 – CLHS: Special Operator QUOTE set-dispatch-macro-characterは、#のような dispatch macro character に続く文字に対して任意の読み取り関数を登録する仕組みです。#qのような二文字の記法を定義するために使います。なお、sub-char が10進数の数字の場合はエラーになります。 – CLHS: Function SET-DISPATCH-MACRO-CHARACTERidentityは Common Lisp の標準関数で、引数をそのまま値として返します。mapcanなどの高階関数に渡す関数が必要なときにも使われます。(eql x (identity x))は任意のxに対して真になりますが、(eq x (identity x))は数値や文字の場合に偽になることがあります。 – CLHS: Function IDENTITY- homoiconicity は「プログラムの内部表現が、そのプログラム自体を読むだけで推論できる」という性質です。Lisp は S式という同一のデータ構造でコードとデータの両方を表現しており、homoiconic な言語の代表例とされています。 – Homoiconicity – Wikipedia