動機
先日、Clojureの長めのマクロを読むという記事を書いたのですが、自分でもびっくりするほど展開時のコードとコンパイル時のコードを区別できていませんでした。
次は何をしようかなーと考えてはいたものの、さすがにもう少しマクロについて理解せずに先に進むわけにもいかないので再度勉強し直すことにしました。
自分が理解していないのはマクロの中でもクォートだと思ったので、今回はクォートに絞って理解を勧めます。
何番煎じかわかりませんが、自分なりの表現でまとめてみます。
'(クォート)の特性
基本的にクォートされた値は評価されません。例を見たほうが早いでしょうか。
'(シングルクォーテーション)はquoteと同じ意味で、実行時に展開されるリーダマクロと呼ばれるものです。
※追記
コメントで指摘いただきましたが、リーダマクロが展開されるのは実行時ではなくリード時でした。
;; 下記は(quote (1 2 3))と同じ意味
'(1 2 3) ;=> (1 2 3)
'{1 2 3 4} ;=> {1 2, 3 4}
'[1 2 3 4] ;=> [1 2 3 4]
'(println :hello) ;=>(println :hello)
'(println (+ 1 1)) ;=>(println (+ 1 1))
普通であればシーケンスの先頭が関数のシンボルの場合は関数が呼び出されますが、呼び出されずにコードがそのままデータとして残っているのがわかると思います。
ベクタ、マップあたりはREPLではわかりにくいですが、ちゃんと評価は遅延されています。途中にシンボルを入れても評価されなかったり、クォートを2つ重ねるときちんとそれがみえることからそれが言えます。
シンプルなシーケンスはlistやconsで代替できる場合があります。
;; 以下を評価した結果はすべて同じになる
(quote (println :hello))
'(println :hello)
;; 関数やマクロは先頭がシンボルのシーケンス
(list 'println :hello)
(cons 'println (cons :hello nil))
なので、公式APIでもクォートを使わないマクロがたまにあります(使い分けの意図まではわからないですが)。
(defmacro when
"Evaluates test. If logical true, evaluates body in an implicit do."
{:added "1.0"}
[test & body]
(list 'if test (cons 'do body)))
クォートされた値を評価したいときはevalを使います。
;; :hello => nil
(eval (quote (println :hello)))
(eval '(println :hello))
;; 同様に先頭がシンボルのシーケンスも評価できる
(eval (list 'println :hello))
(eval (cons 'println (cons :hello nil)))
シンプルなクォートでは名前解決はしません。なので、自分でシンボルをくっつけて1つの式にしても動きます。
後述する`(バッククォート)はグローバルなシンボルの名前解決を行うようです。なので、自分でくっつけて実行してもエラーになります。
;; シンプルなクォート
;; xが動的に評価されている
;; :hello => ni
(eval (list 'let '[x :hello] '(println x)))
;; バッククォート
;; CompilerException
;; 現在のスコープにxが見つからないのでエラー
(eval (list 'let '[x :hello] `(println x)))
;; シンプルなクォートを入れ込み、評価することで乗り切る
;; xが動的に評価されている
(eval (list 'let '[x :hello] `(println ~`x)))
(eval `(let [~'x :hello] (println ~'x)))
あとは式がクォートされているかの判定ができるかどうか考えていたのですが、ちょっと方法を見つけることができなかったです。
その他のクォート
`(バッククォート)
先ほど出てきたものですね。基本的な結果は同じです。
違いはグローバルなシンボルの名前解決と、下にあるいくつかの追加の機能が使えることです。
`(1 2 3) ;=> (1 2 3)
`{1 2 3 4} ;=> {1 2, 3 4}
`[1 2 3 4] ;=> [1 2 3 4]
`(println :hello) ;=>(println :hello)
`(println (+ 1 1)) ;=>(println (+ 1 1))
入れ子にすると展開結果が違います。実行結果は同じらしいです(シンボルを除く)。
;; シンプルなクォート
''(1 2 3) ;=> (quote (1 2 3))
''{1 2 3 4} ;=> (quote {1 2, 3 4})
''[1 2 3 4] ;=> (quote [1 2 3 4])
''(println :hello) ;=>(quote (println :hello))
''(println (+ 1 1)) ;=>(quote (println (+ 1 1)))
;; バッククォート
``(1 2 3) ;=> (clojure.core/seq (clojure.core/concat (clojure.core/list 1) (clojure.core/list 2) (clojure.core/list 3)))
``{1 2 3 4} ;=> (clojure.core/apply clojure.core/hash-map (clojure.core/seq (clojure.core/concat (clojure.core/list 1) (clojure.core/list 2) (clojure.core/list 3) (clojure.core/list 4))))
``[1 2 3 4] ;=> (clojure.core/apply clojure.core/vector (clojure.core/seq (clojure.core/concat (clojure.core/list 1) (clojure.core/list 2) (clojure.core/list 3) (clojure.core/list 4))))
``(println :hello) ;=>(clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/println)) (clojure.core/list :hello)))
``(println (+ 1 1)) ;=>(clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/println)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/+)) (clojure.core/list 1) (clojure.core/list 1))))))
~(チルダ)
チルダはバッククォートされた式の中の一部を評価します。
;; 1が現在のスコープの値で置き換えられている
(let [x 1] `(+ 1 x)) ;=>(clojure.core/+ 1 user/x)
(let [x 1] `(+ 1 ~x)) ;=>(clojure.core/+ 1 1)
;; evalと同じではない
(let [x '1] `(+ 1 ~~x)) ;=> IllegalStateException
(let [x '1] `(+ 1 ~(eval x))) ; (clojure.core/+ 1 1)
;; 入れ子にすれば連続して書くこともできる
(let [x 1] `(do `(println ~~x))) ; (do (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/println)) (clojure.core/list 1))))
~@(アンクォート・スプライシング)
こちらもバッククォートの中で機能します。シーケンスを平坦化してインライン化します。
;; ([10 11])とはならない
(def x [10 11])
`(~@x) ;=> (10 11)
;; こちらも入れ子にすれば連続して書ける
`(list `(~@~@x)) ;;=> (clojure.core/list (clojure.core/seq (clojure.core/concat [10 11])))
クォートとマクロと関数
クォートとマクロ
クォートといえば、マクロで使われることが多いですよね。マクロで返された値は1度だけ評価されます。
マクロの引数も評価されませんが、クォートされているかというとされていないような気がします。
;; :hello => nil
(defmacro aquote[]
'(println :hello))
(aquote)
;; (println :hello)
(defmacro quotes[]
''(println :hello))
(quotes)
;; クォートでなくても同じ
;; :hello => nil
(defmacro alist[]
(list 'println :hello))
(alist)
クォートと関数
マクロ以外の場合は基本的に値は評価されません。評価したい場合は自分でevalするか、チルダで展開したりします。
;; (println :hello)
(defn aquote[]
'(println :hello))
(aquote)
;; クォートでなくても同じ
;; (println :hello)
(defmacro alist[]
(list 'println :hello))
(alist)
関数とマクロのヘルパーの違い
なので、一見フォームを返す式があったとしても、返すのがマクロなのか関数なのかで結果が変わってくるのでヘルパーを作るときなどは注意が必要です。
あまり意味のない例ですが、展開の仕方が少しだけ変わります。
;; ヘルパーマクロ
;; 呼び出すだけでよい
(defmacro macro-helper[]
'(println "hello"))
(defmacro macro-macro[]
(macro-helper)
nil)
;; ヘルパー関数
;; 呼び出した値を~(チルダ)やevalで明示的に展開
(defn fn-helper[]
'(println "hello"))
(defmacro fn-macro[]
`(do ~(fn-helper)))
(fn-macro)
その他
クォートされた式を返す関数を定義するとidentical?がtrueになります。
(defn q[] '(1 2 3 ))
(identical? (q) (q)) ;=> true
(identical? '(1 2 3) '(1 2 3)) ;=> false
感想
前の読むだけの記事よりも大量にREPLを叩いたのでだいぶ理解が進みました。
やはり書くことと読むことは両輪ですね。
クォートに関しては知識より慣れが大きいと思います。もし難しければ、頭を空にして思いつくだけ書いてみるとよいかと思います。