【Common Lisp】
いろんなマクロを読む・書く

関連記事

1. 標準仕様の代表的なマクロを読む

Common Lisp の標準仕様には多くのマクロが含まれています。
自分でマクロを書く前に、どんな発想で作られているかを読むことが参考になります。

1.1. 条件分岐 whenunless

if を読みやすくラップしたシンプルなマクロです。
when は条件が真のときだけ body を実行し、unless は偽のときだけ実行します。

; when の実装イメージ
(defmacro when (condition &body body)
  `(if ,condition (progn ,@body) nil))

; unless の実装イメージ
(defmacro unless (condition &body body)
  `(if ,condition nil (progn ,@body)))Code language: Lisp (lisp)

1.2. 短絡評価 andor

関数なら全引数が先に評価されてしまうため、短絡評価を実現するためにマクロです。
途中で後続する評価を止める必要があるからです1

; and の実装イメージ(再帰展開)
(defmacro and (&rest forms)
  (if (null forms)
      t
      (if (null (cdr forms))
          (car forms)
          `(if ,(car forms) (and ,@(cdr forms)) nil))))Code language: Lisp (lisp)

1.3. 束縛の入れ子 let*

let は並列束縛、let* は逐次束縛です。
実は、let*let を入れ子にした展開に相当しています。

; let* の展開イメージ
(let* ((x 1) (y (+ x 1)))
  y)

; =>
(let ((x 1))
  (let ((y (+ x 1)))
    y))Code language: Lisp (lisp)

1.4. 汎用代入マクロ setf

setf は場所(place)への汎用代入マクロです。
リストの要素、配列、構造体のスロットなど、さまざまな場所に同じ構文で値を書き込めます。

(defparameter *lst* (list 1 2 3))
(setf (car *lst*) 99)
*lst*  ;=> (99 2 3)

(defparameter *arr* (make-array 3 :initial-contents '(1 2 3)))
(setf (aref *arr* 1) 42)
*arr*  ;=> #(1 42 3)Code language: Lisp (lisp)

setfdefine-setf-expander で拡張できます。
自作のデータ構造に setf を対応させたいときに使います。

1.5. 反復言語系 loop

loop は Common Lisp 最大のマクロで、独自のミニ言語を構成しています。
forcollectwhensum といったキーワードを組み合わせて、繰り返し処理を宣言的に書けます。

(loop for i from 1 to 5
      when (oddp i)
        collect (* i i))
;=> (1 9 25)Code language: Lisp (lisp)

内部は複雑な展開をしており、macroexpand で確認すると長い do 形式に変換されます2

1.6. クリーンアップ処理 with-open-file

ファイルを開いてから確実に閉じる処理を包み込んだマクロです。

unwind-protect を使って、エラーが発生してもクリーンアップが走るように展開されます3

(with-open-file (stream "data.txt" :direction :input)
  (read-line stream))Code language: Lisp (lisp)

展開イメージはこうなります。

(let ((stream (open "data.txt" :direction :input)))
  (unwind-protect
      (read-line stream)
    (when stream (close stream))))Code language: Lisp (lisp)

1.7. リスト構築 pushpop

リストをスタックとして使うためのマクロです。
pushsetfcons の組み合わせに展開されます。

; push の展開イメージ
(push item lst)
; => (setf lst (cons item lst))Code language: Lisp (lisp)

1.8. 構造体定義 defstruct

構造体を定義すると同時に、コンストラクタ・アクセサ・述語関数をまとめて生成します。
define- 系マクロの代表例です。

(defstruct point
  x y)

; 自動生成される関数
; make-point, point-x, point-y, point-p, copy-point
(defparameter *p* (make-point :x 3 :y 4))
(point-x *p*)  ;=> 3Code language: Lisp (lisp)

2. alexandria:with-gensyms

複数のgensym を使うときには、Alexandria ライブラリの with-gensyms を使うとすっきり書けます。

(ql:quickload :alexandria)

(defmacro my-dotimes (n &body body)
  (alexandria:with-gensyms (i)
    `(do ((,i 0 (+ ,i 1)))
         ((= ,i ,n))
       ,@body)))Code language: Lisp (lisp)

with-gensyms は指定したシンボル名それぞれに gensym を割り当て、let で束縛します。
(gensym) を手で並べるより意図が明確になります4

3. 実践パターン

3.1. with- 系:リソース管理・コンテキスト切り替え

with- で始まるマクロは「あるコンテキストの中で処理を行う」というパターンです。
標準の with-open-file がその代表です。

独自の with- 系マクロを書くときは、unwind-protect でクリーンアップを保証するのが定石です。
unwind-protect は、エラーが発生してもクリーンアップ節を必ず実行します。

(defmacro with-db-connection (var &body body)
  (alexandria:with-gensyms (conn)
    `(let ((,conn (open-db-connection)))
       (let ((,var ,conn))
         (unwind-protect
             (progn ,@body)
           (close-db-connection ,conn))))))

(with-db-connection (db)
  (query db "SELECT * FROM users"))Code language: Lisp (lisp)

with- 系マクロの構造はほぼ決まっています。
リソースを取得して変数に束縛し、ボディを unwind-protect で包み、クリーンアップを unwind-protect のクリーンアップ節に置きます。

コンテキストの切り替えにも使えます。
たとえば、特定のパラメータを一時的に変更してから元に戻すパターンです。

(defmacro with-logging-level (level &body body)
  (alexandria:with-gensyms (old-level)
    `(let ((,old-level *logging-level*))
       (setf *logging-level* ,level)
       (unwind-protect
           (progn ,@body)
         (setf *logging-level* ,old-level)))))

(with-logging-level :debug
  (process-data input))Code language: Lisp (lisp)

これは let でダイナミック変数を一時的に束縛するパターンとほぼ同じですが、マクロにすることで意図が明確になり、変数名を知らなくても使えます5

3.2. do- 系:繰り返しの抽象

do- で始まるマクロは、繰り返し処理の定型をラップします。
標準の dolistdotimes がその代表です。

独自のデータ構造やファイルフォーマットを走査するときに、do- 系マクロを自作すると呼び出し側がすっきりします。

(defmacro do-lines ((var filename) &body body)
  (alexandria:with-gensyms (stream)
    `(with-open-file (,stream ,filename :direction :input)
       (loop for ,var = (read-line ,stream nil nil)
             while ,var
             do (progn ,@body)))))

(do-lines (line "data.txt")
  (when (search "error" line)
    (print line)))Code language: Lisp (lisp)

do-lines はファイルを1行ずつ走査する定型を隠しています。
呼び出し側は line に何をするかだけ書けばよく、ファイルのオープンとクローズ、read-line の終端判定を意識しなくてよくなります。

ハッシュテーブルを走査する例も同様です。

(defmacro do-hash ((key val table) &body body)
  `(maphash (lambda (,key ,val) ,@body) ,table))

(defparameter *scores* (make-hash-table))
(setf (gethash :alice *scores*) 85)
(setf (gethash :bob *scores*) 90)

(do-hash (name score *scores*)
  (format t "~a: ~a~%" name score))
; :ALICE: 85
; :BOB: 90Code language: Lisp (lisp)

3.3. define- 系:定型コードをまとめて生成する

define-def で始まるマクロは、定義をまとめて生成します。
defstruct がその代表ですが、独自の定型を自動生成させることもできます。

たとえば、バリデーション付きのアクセサをまとめて定義するケースです。

(defmacro define-validated-accessor (name predicate error-msg)
  (let ((getter (intern (format nil "GET-~a" name)))
        (setter (intern (format nil "SET-~a" name)))
        (storage (intern (format nil "*~a*" name))))
    `(progn
       (defparameter ,storage nil)
       (defun ,getter () ,storage)
       (defun ,setter (val)
         (unless (funcall ,predicate val)
           (error ,error-msg))
         (setf ,storage val)))))

(define-validated-accessor
  age
  #'(lambda (x) (and (integerp x) (>= x 0) (<= x 150)))
  "Age must be an integer between 0 and 150")

(set-age 25)
(get-age)   ;=> 25

(set-age -1)
; Error: Age must be an integer between 0 and 150Code language: Lisp (lisp)

intern は文字列からシンボルを作ります。
定義名を動的に生成するときに使います6

同じパターンの関数やクラスが増えてきたと感じたとき、define- 系マクロへの切り出しを検討するサインです。

4. 開発ツール系(eval-when)

eval-when と環境判定を組み合わせると、開発環境でだけ動くコードを書けます。

eval-when はいつそのフォームを評価するかを制御するマクロで、:compile-toplevel:load-toplevel:execute の3つのタイミングを指定できます7

SLIMEのバックエンドである SWANK が接続中かどうかは find-package :swank で判定できます。
SLIME接続中はこのパッケージがロードされています。

この2つを組み合わせて、C-c C-k(ファイルのコンパイルとロード)のタイミングだけでテストを自動実行するマクロを作れます。

(defmacro dev-test (name &body body)
  `(progn
     (parachute:define-test ,name ,@body)
     (eval-when (:load-toplevel)
       (when (find-package :swank)
         (parachute:test ',name)))))Code language: Lisp (lisp)

使い方は関数の直後に置くだけです。

(defun normalize-name (s)
  (string-capitalize s))

(dev-test normalize-name-test
  (parachute:is string= "Alice" (normalize-name "alice"))
  (parachute:is string= "Bob Smith" (normalize-name "bob smith")))Code language: Lisp (lisp)

C-c C-k するたびに normalize-name-test が自動で走ります。
CI環境では find-package :swank が偽になるため、dev-test 内の自動実行ブロックは無視されます。
CIでは ASDF の test-system を通してテストを明示的に実行する設計になります。
dev-test で検索すれば全テストが見つかるので、本番ビルドから外すのも簡単です。

4.1. with-timing

デバッグ用に実行時間を計測したいときも同じ発想で書けます。

(defmacro with-timing (label &body body)
  (alexandria:with-gensyms (start result)
    `(let ((,start (get-internal-real-time)))
       (let ((,result (progn ,@body)))
         (format t "~a: ~,3f sec~%"
                 ,label
                 (/ (- (get-internal-real-time) ,start)
                    internal-time-units-per-second))
         ,result))))

(with-timing "sort"
  (sort (copy-list *large-list*) #'<))
; sort: 0.023 secCode language: Lisp (lisp)

5. 型宣言と最適化:declareを隠す

Common Lisp は動的型付けですが、declare で型情報をコンパイラに伝えると数値演算などを大幅に高速化できます。ただし毎回書くには冗長で、書き忘れも起きやすいです。マクロで隠すと宣言の漏れがなくなります8

素朴に書くとこうなります。

(let ((arr (make-array 1000 :element-type '(unsigned-byte 8))))
  (declare (type (simple-array (unsigned-byte 8) (*)) arr))
  (loop for i below 1000
        do (setf (aref arr i) (mod i 256))))Code language: Lisp (lisp)

make-arraydeclare の型が一致している必要があり、両方を正確に書くのは手間です。

5.1. 型付き配列を作るマクロ

よく使う型について、生成と宣言をセットにしたマクロを定義します。

(defmacro with-u8-array ((var size) &body body)
  `(let ((,var (make-array ,size :element-type '(unsigned-byte 8)
                                 :initial-element 0)))
     (declare (type (simple-array (unsigned-byte 8) (*)) ,var))
     ,@body))

(defmacro with-f64-array ((var size) &body body)
  `(let ((,var (make-array ,size :element-type 'double-float
                                 :initial-element 0.0d0)))
     (declare (type (simple-array double-float (*)) ,var))
     ,@body))Code language: Lisp (lisp)

呼び出し側は型を一箇所だけ意識すれば済みます。

(with-u8-array (buf 1024)
  (read-sequence buf stream)
  (process buf))

(with-f64-array (data 512)
  (loop for i below 512
        do (setf (aref data i) (compute i))))Code language: Lisp (lisp)

5.2. 型ごとに定義を生成するマクロ

型が増えたとき、define- 系マクロで with-xxx-array をまとめて生成できます。

(defmacro define-array-macro (name element-type initial)
  (let ((macro-name (intern (format nil "WITH-~a-ARRAY" name))))
    `(defmacro ,macro-name ((var size) &body body)
       `(let ((,var (make-array ,size
                                :element-type ',',element-type
                                :initial-element ,',initial)))
          (declare (type (simple-array ,',element-type (*)) ,var))
          ,@body))))

(define-array-macro u8  (unsigned-byte 8)  0)
(define-array-macro u32 (unsigned-byte 32) 0)
(define-array-macro f32 single-float       0.0)
(define-array-macro f64 double-float       0.0d0)Code language: Lisp (lisp)

これで with-u8-arraywith-u32-arraywith-f32-arraywith-f64-array が一度に定義されます。

5.3. 数値演算の最適化

配列操作だけでなく、数値演算全体に declare を使えます。
locally を使うと、ある範囲だけ最適化宣言を有効にできます。

(defmacro with-fast-math (&body body)
  `(locally
     (declare (optimize (speed 3) (safety 0) (debug 0)))
     ,@body))

(with-fast-math
  (loop for i of-type fixnum below (length arr)
        sum (aref arr i) of-type fixnum))Code language: Lisp (lisp)

(safety 0) は境界チェックを外すため、範囲外アクセスが未定義動作になります。パフォーマンスが必要な内側のループに限定して使います9

the フォームで式の型をコンパイラに伝えることもできます。
マクロで隠すと毎回書かなくて済みます。

(defmacro the-fixnum (expr)
  `(the fixnum ,expr))

(defmacro the-f64 (expr)
  `(the double-float ,expr))

; 使い方
(the-fixnum (+ a b))
(the-f64 (* x 2.0d0))Code language: Lisp (lisp)

declarethe を使った最適化は、SBCL のコンパイラノートを見ながら進めるのが実際の手順です。
(declaim (optimize (speed 1) (debug 3))) で開発中はデバッグを優先し、ボトルネックが特定できてから with-fast-math を適用する、という使い分けが現実的です10

6. DSL:Lispの構文で独自言語を作る

マクロを使うと、Common Lisp の構文の中に独自のミニ言語(DSL)を埋め込めます。
loop のようなミニ言語を自分で作るイメージです。

DSLを作るときの基本的な流れは、「使いたい構文を先に書いてから、それを展開する defmacro を書く」という順番が自然です11

たとえば、ルーティングテーブルをこう書けるようにしたいとします。

(define-routes
  (GET  "/users"     #'list-users)
  (POST "/users"     #'create-user)
  (GET  "/users/:id" #'get-user))Code language: Lisp (lisp)

これを実現するマクロです。

(defparameter *routes* '())

(defmacro define-routes (&body routes)
  `(progn
     (setf *routes* '())
     ,@(mapcar (lambda (route)
                 (destructuring-bind (method path handler) route
                   `(push (list ',method ,path ,handler) *routes*)))
               routes)))Code language: Lisp (lisp)

destructuring-bind でルートの各要素を分解し、mapcar でそれぞれを push するコードに変換しています。

6.1. テストフレームワーク

テストフレームワークの構文も同じ発想で作れます。

(defmacro define-suite (name &body tests)
  `(progn
     ,@(mapcar (lambda (test)
                 (destructuring-bind (test-name . body) test
                   `(defun ,(intern (format nil "TEST-~a" test-name)) ()
                      ,@body)))
               tests)
     (defun ,(intern (format nil "RUN-~a" name)) ()
       ,@(mapcar (lambda (test)
                   `(,(intern (format nil "TEST-~a" (car test)))))
                 tests))))

(define-suite math-tests
  (addition
    (assert (= 4 (+ 2 2))))
  (subtraction
    (assert (= 0 (- 2 2)))))

(run-math-tests)Code language: Lisp (lisp)

7. nil ハンドリング:or-return・or-setf・if-let

Common Lisp で nil を扱うパターンは3通りに整理できます。
nil だったら早期リターン、nil だったらデフォルト値をセット、nil でなければ束縛して使う、の3つです。
それぞれをマクロで表現します。

7.1. or-return:nilなら即座に抜ける

関数の途中で nil が返ってきたとき、そのまま抜けたい場面があります。
loop の中で return を使う形で実装します。

(defmacro or-return (expr &optional (default nil))
  (alexandria:with-gensyms (val)
    `(let ((,val ,expr))
       (or ,val (return ,default)))))

(defun first-even (items)
  (loop for item in items do
    (or-return (when (evenp item) item)))
  nil)

(first-even '(1 3 5 6 7))  ;=> 6
(first-even '(1 3 5 7))    ;=> NILCode language: Lisp (lisp)

Rust の ? 演算子や Ruby の return if nil に対応するパターンです。
loop の外で使う場合は blockreturn-from を組み合わせる必要があります。

7.2. or-setf:nilならデフォルト値をセット

変数が nil のときだけ値をセットします。
Ruby の ||= に対応します。

(defmacro or-setf (place value)
  `(unless ,place
     (setf ,place ,value)))

(defparameter *cache* nil)

(or-setf *cache* (build-cache))
*cache*  ; build-cache の結果が入っている

(or-setf *cache* (build-cache))
*cache*  ; すでに値があるので build-cache は呼ばれないCode language: Lisp (lisp)

value*cache*nil のときだけ評価されます。
関数では (build-cache) が常に評価されてしまうので、マクロにする意味があります。

incfdecf と命名規則を合わせて defaultf と呼ぶこともあります。

(defmacro defaultf (place value)
  `(unless ,place
     (setf ,place ,value)))Code language: Lisp (lisp)

7.3. if-let:nilでなければ束縛して使う

条件式の結果が nil でないとき、その値を変数に束縛してボディを実行します。
Swift や Kotlin の if let に相当します。

(defmacro if-let ((var expr) then &optional else)
  (alexandria:with-gensyms (val)
    `(let ((,val ,expr))
       (if ,val
           (let ((,var ,val))
             ,then)
           ,else))))

(if-let (user (find-user :id 42))
  (format t "found: ~a~%" (user-name user))
  (format t "not found~%"))Code language: Lisp (lisp)

find-usernil を返したとき else 節を実行し、nil 以外を返したとき user に束縛して then 節を実行します。
val に一時束縛してから var に渡しているのは、expr の多重評価を防ぐためです。

when-letelse 節なしの版です。

(defmacro when-let ((var expr) &body body)
  (alexandria:with-gensyms (val)
    `(let ((,val ,expr))
       (when ,val
         (let ((,var ,val))
           ,@body)))))

(when-let (match (find-if #'evenp numbers))
  (format t "first even: ~a~%" match)
  (push match *results*))Code language: Lisp (lisp)

Alexandria に if-letwhen-let が標準で入っているので、実用では (alexandria:if-let ...) を使うのが手軽です。自作する意義は、実装を読むことでマクロの構造を理解できる点にあります12

8. Paul Graham スタイル(On Lisp より)

Paul Graham の On Lisp が紹介するマクロパターンは、Lispのマクロ技法のリファレンスとして今も参照されます13

8.1. 関数合成

compose はいくつかの関数を合成して1つの関数にします。
関数版で書けますが、マクロ版はオーバーヘッドなしで展開できる利点があります。

(defmacro compose (&rest fns)
  (let ((arg (gensym)))
    `(lambda (,arg)
       ,(reduce (lambda (acc fn)
                  `(,fn ,acc))
                (reverse fns)
                :initial-value arg))))

(funcall (compose string-upcase string-trim) "  hello  ")
;=> "HELLO"Code language: Lisp (lisp)

8.2. マクロパターンを生成する

似たパターンのマクロが増えてきたとき、マクロを生成するマクロで定義をまとめられます。

(defmacro define-anaphor (name base-macro)
  `(defmacro ,name (condition &body body)
     `(aif ,condition (progn ,@body))))

(define-anaphor awhen when)
(define-anaphor awhile while)Code language: Lisp (lisp)

9. Doug Hoyte スタイル(Let Over Lambdaより)

Doug Hoyte の Let Over Lambda は、クロージャとマクロを組み合わせた高度なパターンを扱います。

タイトルの「Let Over Lambda」はクロージャを作るパターン名で、let のスコープを閉じ込めた lambda を返す構造を指しています14

9.1. let-over-lambda(状態を持つオブジェクト)

クロージャをマクロで包むと、状態を持つオブジェクトを簡潔に作れます。

(defmacro make-counter (&optional (start 0))
  `(let ((count ,start))
     (lambda ()
       (incf count))))

(defparameter *c* (make-counter))
(funcall *c*)  ;=> 1
(funcall *c*)  ;=> 2
(funcall *c*)  ;=> 3

(defparameter *c2* (make-counter 100))
(funcall *c2*) ;=> 101Code language: Lisp (lisp)

複数のクロージャで同じ let スコープを共有させると、内部状態を持つオブジェクトのように振る舞います。

(defmacro make-stack ()
  `(let ((storage '()))
     (list
       (lambda (x) (push x storage))   ; push
       (lambda ()  (pop storage))       ; pop
       (lambda ()  storage))))          ; inspect

(defparameter *s* (make-stack))
(funcall (first *s*) 1)
(funcall (first *s*) 2)
(funcall (second *s*))  ;=> 2
(funcall (third *s*))   ;=> (1)Code language: Lisp (lisp)

9.2. パンドリックマクロ(クロージャにメッセージを送る)

パンドリックマクロは、クロージャの内部状態を外部から読み書きできるようにします。

通常、クロージャの内部変数は外からアクセスできませんが、パンドリックマクロはその制限を意図的に破る仕組みです。

(defmacro plambda (largs pargs &body body)
  (let ((pargs (mapcar #'list pargs)))
    `(let (,@pargs)
       (dlambda
         (:pandoric-get (sym)
           (case sym
             ,@(mapcar (lambda (pa)
                         `(,(car pa) ,(car pa)))
                       pargs)))
         (:pandoric-set (sym val)
           (case sym
             ,@(mapcar (lambda (pa)
                         `(,(car pa) (setf ,(car pa) val)))
                       pargs)))
         (t ,@body)))))Code language: Lisp (lisp)

完全な実装は複雑ですが、発想は「クロージャにメッセージを送ることで内部変数にアクセスする」というものです。

9.3. リーダーマクロ(set-dispatch-macro-character)

リーダーマクロは、Lispリーダーが文字列を読み込む段階で動くマクロです。
# で始まるリーダーマクロ(ディスパッチマクロ文字)を定義すると、新しいリテラル表記を追加できます。

; #[ ... ] で正規表現リテラルを作る例(概念的なイメージ)
(set-dispatch-macro-character
  #\# #\[
  (lambda (stream char1 char2)
    (declare (ignore char1 char2))
    (let ((pattern (read-until stream #\])))
      `(cl-ppcre:create-scanner ,pattern))))

; 使うと #[a-z]+ のように書けるCode language: Lisp (lisp)

リーダーマクロは強力ですが、コードの可読性に影響するため慎重に使います15

10. Peter Norvig スタイル(Paradigms of AI Programmingより)

Peter Norvig は「マクロは本当に必要なときだけ使え(Use macros if really necessary)」と書いていて16PAIP(Paradigms of AI Programming)では、マクロを使う場面と使わない場面を区別します。

10.1. 関数で書ける場合はマクロにしない

まずは、マクロを使う前に、関数で書けないかを確認することが大切です。

; マクロにする必要がない例
(defmacro add (a b) `(+ ,a ,b))   ; 不要:関数で十分
(defun add (a b) (+ a b))         ; こちらで書く

; マクロにする必要がある例
(defmacro when (condition &body body)
  `(if ,condition (progn ,@body)))  ; 評価タイミングの制御が必要Code language: Lisp (lisp)

判断基準はシンプルです。

  • 評価タイミングを制御する、
  • 式のテキストを扱う、
  • 構文を変える

という3つのどれかが必要なときだけマクロを選びます。それ以外は関数の方が、デバッグしやすく、mapcar に渡せて、コンパイラの最適化も効きます17

10.2. パターンマッチング

すべてをマクロで解決するのではなく、マクロと関数を組み合わせた設計も有効です。

Norvig が PAIP で示したパターンマッチャーは、マクロと関数を適切に組み合わせた設計の好例です。

(defmacro match-case (expr &body clauses)
  (alexandria:with-gensyms (val)
    `(let ((,val ,expr))
       (cond
         ,@(mapcar (lambda (clause)
                     (destructuring-bind (pattern . body) clause
                       `((match-p ',pattern ,val)
                         ,@body)))
                   clauses)))))

; パターンマッチの述語は関数で書く
(defun match-p (pattern val)
  (cond
    ((eq pattern '?) t)
    ((symbolp pattern) t)
    ((and (consp pattern) (consp val))
     (and (match-p (car pattern) (car val))
          (match-p (cdr pattern) (cdr val))))
    (t (equal pattern val))))

(match-case '(1 2 3)
  ((1 ? 3) (print "matched!"))
  (? (print "anything")))
; "matched!"Code language: Lisp (lisp)

構文の部分(match-case)はマクロで、マッチングのロジック(match-p)は関数で書いています。
ロジックを関数に分けることで、テストしやすく、REPLで単体確認もできます。

マクロをどこに使ってどこに使わないか、その境界線を意識することが、読みやすく保守しやすいマクロを書くための判断力になります。

  1. and は全フォームが真のとき最後のフォームの値を返し、偽のフォームに出会った時点で nil を返します。or は最初に真となったフォームの値を返します。この「値を返す」という仕様も、単なるブール演算子ではなくマクロとして設計された理由の一つです。 – Common Lisp HyperSpec: and
  2. loop マクロは Interlisp の Warren Teitelman が実装した繰り返し構造に触発されたもので、その後 Lisp Machine や MacLisp を経て Common Lisp に取り込まれました。独自のミニ言語を持つ設計は当時から議論を呼んでおり、好みが分かれます。 – Common Lisp HyperSpec: History
  3. unwind-protect のクリーンアップ節自体は保護されていません。クリーンアップ節の中で非ローカル脱出が発生しても特別な処理は行われないと CLHS に明記されています。そのためクリーンアップ節の中でエラーが起きると意図しない動作になることがあります。 – Common Lisp HyperSpec: unwind-protect
  4. Alexandria は Common Lisp の事実上の標準ユーティリティライブラリです。with-gensyms のほかに if-letwhen-letensure-list など実用的なマクロ・関数を多数含みます。Quicklisp で (ql:quickload :alexandria) と打つだけで使えます。 – Alexandria documentation
  5. ダイナミック変数への let 束縛はスレッドセーフではない場合があります。マルチスレッド環境では bordeaux-threads などのライブラリが提供するスレッドローカルな束縛機構を検討してください。unwind-protect はスレッドをまたぐ非ローカル脱出は保護しません。 – Common Lisp HyperSpec: Special Variables
  6. intern は引数のパッケージを省略すると *package*(現在のパッケージ)にシンボルを作ります。マクロが別パッケージで呼ばれる場合は意図しないパッケージにシンボルが作られることがあります。パッケージを明示するか、make-symbol で非インターンシンボルを使う方法もあります。 – Common Lisp HyperSpec: intern
  7. :compile-toplevel はコンパイル時にコンパイラがフォームを評価するタイミング、:load-toplevel はコンパイル済みファイルがロードされるタイミング、:executeeval や REPL での直接評価のタイミングです。C-c C-k:load-toplevel に相当します。 – Common Lisp HyperSpec: eval-when
  8. declare は Common Lisp の特殊フォームで、変数の型・最適化レベル・動的宣言などをコンパイラに伝えます。関数の先頭や let の直後に置きます。SBCL では (declaim (optimize (speed 3) (safety 1))) のように大域的な最適化レベルを設定することもできます。 – Common Lisp HyperSpec: declare
  9. optimize の品質指標は speedsafetydebugspacecompilation-speed の5つで、値は 0〜3 の整数です。(safety 0) は型チェックや境界チェックを省略するため、バグが潜んでいると診断が困難になります。本番コードでは (safety 1) 以上を保つのが現実的です。 – Common Lisp HyperSpec: optimize
  10. SBCL のコンパイラノートは *compiler-notes* に記録されており、(sb-ext:compiler-notes ...) で確認できます。型推論が不完全な箇所に「unable to optimize」などのノートが出るため、これを手がかりに declarethe を加えていきます。SBCL の最適化については公式マニュアルの「Compiler」章が詳しいです。

    • SBCL Manual: Compiler
    • Common Lisp の loop 自体がこのアプローチの最大の例で、独自のキーワード体系を持つ本格的なDSLです。loop の設計を分析することは、自作DSLの設計の参考になります。Practical Common Lisp の第22章がループDSLの詳細な解説として参考になります。 – Practical Common Lisp: Chapter 22
    • Alexandria の if-let は複数の束縛を並べることができます。(alexandria:if-let ((x expr1) (y expr2)) then else) と書くと、xy がともに非 nil のときだけ then を実行します。単一束縛の場合とは動作が異なるため注意が必要です。 – Alexandria: if-let
    • On Lisp は Paul Graham が公式に無料公開しており、PDF で入手できます。1993年の出版ですが内容は今も有効で、特にアナフォリックマクロとマクロ生成マクロの章は必読です。 – On Lisp by Paul Graham(無料PDF)
    • Let Over Lambda は著者サイトで購入できます。最初の3章が無料で公開されており、クロージャとマクロの基礎を扱っています。On Lisp を読んでから読むことが推奨されています。 – Let Over Lambda by Doug Hoyte
    • リーダーマクロを複数のファイルやライブラリで安全に共存させるには named-readtables ライブラリが便利です。グローバルなリードテーブルを書き換えず、名前付きリードテーブルとして管理できます。 – named-readtables on GitHub
    • PAIP は著者の Peter Norvig が GitHub でソースコードを MIT ライセンスで公開しています。書籍はまだ絶版ではありませんが、コードはオープンソースで参照できます。 – PAIP on GitHub
    • Common Lisp Cookbook の macros ページにも「関数で書けるならマクロにするな」という指針がまとめられています。Peter Seibel の Practical Common Lisp 第8章も、マクロと関数の選択基準として参考になります。 – Common Lisp Cookbook: Macros