common-lisp
マクロ
サーチ…
備考
マクロの目的
マクロは、コードの生成、コードの変換、および新しい記法の提供を目的としています。これらの新しい表記法は、ドメインレベルの構造や新しい組み込み言語全体を提供するなど、プログラムをよりよく表現するのに適しています。
マクロはソースコードを自明にすることができますが、デバッグは難しくなります。経験則として、通常の関数ではマクロを使用すべきではありません。それらを使用するときは、通常の落とし穴を避け、よく使われるパターンと命名規則に固執しようとします。
マクロ展開順序
関数と比較すると、マクロは逆の順序で展開されます。最初から最後までつまり、デフォルトでは、外部マクロに必要な構文を生成するために内部マクロを使用することはできません。
評価オーダー
時には、マクロはユーザー提供のフォームを移動する必要があります。評価の順序を変更しないようにする必要があります。ユーザーは、順番に起こっている副作用に頼っている可能性があります。
一度だけ評価する
マクロの展開では、同じユーザー提供フォームの値を複数回使用する必要があります。フォームに副作用が発生したり、高価な機能が呼び出されたりする可能性があります。したがって、マクロはそのようなフォームを一度だけ評価するようにしなければなりません。通常、これはローカル変数( GENSYM
edという名前)に値を代入することによって行われます。
EVAL-WHENを使用してマクロで使用される関数
複雑なマクロは、ロジックの一部が別々の機能で実装されることがあります。しかし、実際のコードがコンパイルされる前にマクロが展開されていることを覚えておく必要があります。ファイルをコンパイルするとき、デフォルトでは、同じファイルに定義されている関数と変数は、マクロの実行中は使用できません。マクロによって使用される同じファイル内のすべての関数と変数の定義は、 EVAL-WHEN
-formの中にラップされなければなりません。 EVAL-WHEN
は、ロードと実行時に囲まれたコードも評価する必要がある場合に、3回指定する必要があります。
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun foobar () ...))
これはマクロの展開から呼び出される関数には当てはまりません。マクロ自体から呼び出される関数にのみ適用されます。
一般的なマクロパターン
TODO:説明を説明に移し、別々に例を追加する
FOOF
Common Lispには、 一般化参照の概念があります。プログラマは変数を変数と同じようにさまざまな「場所」に値を設定できます。この能力を利用するマクロは、しばしばその名前にF
-postfixを持つ。この場所は通常、マクロの最初の引数です。
標準からの例: INCF
、 DECF
、 ROTATEF
、 SHIFTF
、 REMF
。
愚かな例、ある場所でナンバーストアのサインを反転させるマクロ:
(defmacro flipf (place)
`(setf ,place (- ,place)))
WITH-FOO
リソースを獲得して安全に解放するマクロは、通常、 WITH-
名前が付けられます。マクロは通常次のような構文を使用します:
(with-foo (variable details-of-the-foo...)
body...)
標準からの例: WITH-OPEN-FILE
、 WITH-OPEN-STREAM
、 WITH-INPUT-FROM-STRING
、 WITH-OUTPUT-TO-STRING
。
名前の汚染や意図しない多重評価の落とし穴のいくつかを回避できるこのタイプのマクロを実装する1つのアプローチは、まず機能バージョンを実装することです。例えば、実装の最初のステップwith-widget
安全にウィジェットを作成し、その後クリーンアップマクロ関数であるかもしれません。
(defun call-with-widget (args function)
(let ((widget (apply #'make-widget args))) ; obtain WIDGET
(unwind-protect (funcall function widget) ; call FUNCTION with WIDGET
(cleanup widget) ; cleanup
これは関数であるため、 関数またはサプライヤ内の名前の範囲について懸念がなく、対応するマクロを簡単に記述できます。
(defmacro with-widget ((var &rest args) &body body)
`(call-with-widget (list ,@args) (lambda (,var) ,@body)))
DO-FOO
何かを反復するマクロは、しばしばDO
プレフィックスで名前が付けられます。マクロ構文は、通常、形式でなければなりません
(do-foo (variable the-foo-being-done return-value)
body...)
標準からの例: DOTIMES
、 DOLIST
、 DO-SYMBOLS
。
フットケース、エフケース、CFOOCASE
入力と特定のケースを照合するマクロは、しばしばCASE
-postfixで名前が付けられます。 E...CASE
入力がいずれのケースとも一致しない場合はエラーを通知し、 C...CASE
は継続可能なエラーを通知するC...CASE
です。彼らは次のような構文を持つべきです
(foocase input
(case-to-match-against (optionally-some-params-for-the-case)
case-body-forms...)
more-cases...
[(otherwise otherwise-body)])
標準からの例: CASE
、 TYPECASE
、 HANDLER-CASE
。
たとえば、文字列と正規表現を照合し、レジスタグループを変数にバインドするマクロ。 CL-PPCREを正規表現に使用します。
(defmacro regexcase (input &body cases)
(let ((block-sym (gensym "block"))
(input-sym (gensym "input")))
`(let ((,input-sym ,input))
(block ,block-sym
,@(loop for (regex vars . body) in cases
if (eql regex 'otherwise)
collect `(return-from ,block-sym (progn ,vars ,@body))
else
collect `(cl-ppcre:register-groups-bind ,vars
(,regex ,input-sym)
(return-from ,block-sym
(progn ,@body))))))))
(defun test (input)
(regexcase input
("(\\d+)-(\\d+)" (foo bar)
(format t "Foo: ~a, Bar: ~a~%" foo bar))
("Foo: (\\w+)$" (foo)
(format t "Foo: ~a.~%" foo))
(otherwise (format t "Didn't match.~%"))))
(test "asd 23-234 qwe")
; Foo: 23, Bar: 234
(test "Foo: Foobar")
; Foo: Foobar.
(test "Foo: 43 - 23")
; Didn't match.
DEFINE-FOO、DEFFOO
物を定義するマクロは、通常、 DEFINE-
またはDEF
DEFINE-
いずれかの名前が付けられます。
標準からの例: DEFUN
、 DEFMACRO
、 DEFINE-CONDITION
アナフォリックマクロ
Anaphoricマクロは、ユーザー提供のフォームの結果をキャプチャする変数(しばしばIT
)を導入するマクロです。一般的な例はAnaphoric Ifです。これは通常のIF
と似ていますが、テストフォームの結果を参照する変数IT
も定義しています。
(defmacro aif (test-form then-form &optional else-form)
`(let ((it ,test-form))
(if it ,then-form ,else-form)))
(defun test (property plist)
(aif (getf plist property)
(format t "The value of ~s is ~a.~%" property it)
(format t "~s wasn't in ~s!~%" property plist)))
(test :a '(:a 10 :b 20 :c 30))
; The value of :A is 10.
(test :d '(:a 10 :b 20 :c 30))
; :D wasn't in (:A 10 :B 20 :C 30)!
MACROEXPAND
マクロ展開とは、マクロを実際のコードに変換するプロセスです。これは、通常、コンパイルプロセスの一部として発生します。コンパイラは実際にコードをコンパイルする前にすべてのマクロフォームを展開します。マクロ展開は、Lispコードの解釈中にも起こります。
MACROEXPAND
手動で呼び出して、マクロフォームが展開されているものを確認することができます。
CL-USER> (macroexpand '(with-open-file (file "foo")
(do-something-with file)))
(LET ((FILE (OPEN "foo")) (#:G725 T))
(UNWIND-PROTECT
(MULTIPLE-VALUE-PROG1 (PROGN (DO-SOMETHING-WITH FILE)) (SETQ #:G725 NIL))
(WHEN FILE (CLOSE FILE :ABORT #:G725))))
MACROEXPAND-1
は同じですが、一度しか展開されません。これは、別のマクロフォームに展開するマクロフォームを理解しようとするときに便利です。
CL-USER> (macroexpand-1 '(with-open-file (file "foo")
(do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))
MACROEXPAND
もMACROEXPAND-1
も、すべてのレベルでLispコードを展開しないことに注意してください。トップレベルのマクロフォームのみを展開します。すべてのレベルでフォームをマクロ展開するには、 コードウォーカーが必要です。この機能は、Common Lisp標準では提供されていません。
バッククォート - マクロのコードテンプレートを書く
マクロはコードを返します。 Lispのコードはリストで構成されているので、通常のリスト操作関数を使ってそれを生成することができます。
;; A pointless macro
(defmacro echo (form)
(list 'progn
(list 'format t "Form: ~a~%" (list 'quote form))
form))
これは、特に長いマクロでは、読みにくいことがよくあります。 Backquote readerマクロを使うと 、要素を選択的に評価することで記入された引用テンプレートを書くことができます。
(defmacro echo (form)
`(progn
(format t "Form: ~a~%" ',form)
,form))
(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))
このバージョンは通常のコードとほとんど同じです。コンマはFORM
を評価するために使用されます。他のすべてはそのまま返されます。 ',form
は、単一引用符がコンマの外にあるので、それが返されることに注意してください。
その位置のリストをスプライスするため,@
を使用することもできます。
(defmacro echo (&rest forms)
`(progn
,@(loop for form in forms collect `(format t "Form: ~a~%" ,form))
,@forms))
(macroexpand '(echo (+ 3 4)
(print "foo")
(random 10)))
;=> (PROGN
; (FORMAT T "Form: ~a~%" (+ 3 4))
; (FORMAT T "Form: ~a~%" (PRINT "foo"))
; (FORMAT T "Form: ~a~%" (RANDOM 10))
; (+ 3 4)
; (PRINT "foo")
; (RANDOM 10))
バッククォートは、マクロの外部でも使用できます。
マクロ内の名前の衝突を防ぐ固有の記号
マクロを展開すると、ユーザーが引数として渡さなかったシンボル(たとえば、ローカル変数の名前など)を使用する必要があります。そのような記号が、周囲のコードで使用している記号と衝突しないようにする必要があります。
これは通常、 GENSYM
を使用することで実現されますGENSYM
は、 GENSYM
されていないシンボルを返します。
悪い
以下のマクロを考えてみましょう。それはDOTIMES
、ボディの結果をリストにまとめ、最後に返されます。
(defmacro dotimes+collect ((var count) &body body)
`(let ((result (list)))
(dotimes (,var ,count (nreverse result))
(push (progn ,@body) result))))
(dotimes+collect (i 5)
(format t "~a~%" i)
(* i i))
; 0
; 1
; 2
; 3
; 4
;=> (0 1 4 9 16)
これはこの場合にはうまくいくようですが、ユーザーが本体で使用する変数名RESULT
を使用した場合、結果はおそらくユーザーが期待するものではないでしょう。この試みは、 N
までのすべての整数の合計のリストを収集する関数を記述しようとします。
(defun sums-upto (n)
(let ((result 0))
(dotimes+collect (i n)
(incf result i))))
(sums-upto 10) ;=> Error!
良い
問題を修正するには、 GENSYM
を使用して、マクロ展開でRESULT
変数の一意の名前を生成する必要があります。
(defmacro dotimes+collect ((var count) &body body)
(let ((result-symbol (gensym "RESULT")))
`(let ((,result-symbol (list)))
(dotimes (,var ,count (nreverse ,result-symbol))
(push (progn ,@body) ,result-symbol)))))
(sums-upto 10) ;=> (0 1 3 6 10 15 21 28 36 45)
TODO:文字列からシンボルを作る方法
TODO:異なるパッケージのシンボルに関する問題の回避
if-let、when-let、 - レットマクロ
これらのマクロは、制御フローとバインディングをマージします。彼らは、アノテーション匿名マクロよりも改良されたものです。なぜなら、開発者は名前をつけて意味を伝えることができるからです。そのようなものとして、その使用は、彼らの言い換えられた対応物よりも推奨される。
(if-let (user (get-user user-id))
(show-dashboard user)
(redirect 'login-page))
FOO-LET
マクロは1つ以上の変数をバインドし、対応する条件式( IF
、 WHEN
)のテストフォームとして使用します。複数の変数はAND
結合されます。選択されたブランチは、バインディングを有効にして実行されます。 IF-LET
単純な変数実装は次のようになります。
(defmacro if-let ((var test-form) then-form &optional else-form)
`(let ((,var ,test-form))
(if ,var ,then-form ,else-form)))
(macroexpand '(if-let (a (getf '(:a 10 :b 20 :c 30) :a))
(format t "A: ~a~%" a)
(format t "Not found.~%")))
; (LET ((A (GETF '(:A 10 :B 20 :C 30) :A)))
; (IF A
; (FORMAT T "A: ~a~%" A)
; (FORMAT T "Not found.~%")))
Alexandriaライブラリには、複数の変数をサポートするバージョンが用意されています。
マクロを使用したデータ構造の定義
マクロの一般的な使用方法は、一般的な規則に従いますが、異なるフィールドを含む可能性があるデータ構造用のテンプレートを作成することです。マクロを書くことで、定型コードを繰り返すことなく、データ構造の詳細な構成を指定することができます。メモリの効率の悪い構造(ハッシュなど)を純粋にプログラミングを単純化するために使用することもできません。
たとえば、getterとsetterをそれぞれ持つさまざまなプロパティの範囲を持つ多数のクラスを定義したいとします。さらに、これらのプロパティの一部(ただしすべてではない)については、プロパティが変更されたことを通知するメソッドをセッターに呼び出させたいと考えています。 Common LISPはすでにgetterとsetterを作成するための省略形を持っていますが、このように標準カスタムセッターを書くには、通常、すべてのセッターで通知メソッドを呼び出すコードを複製する必要があります。 。しかし、マクロを定義することによって、はるかに簡単になります:
(defmacro notifier (class slot)
"Defines a setf method in (class) for (slot) which calls the object's changed method."
`(defmethod (setf ,slot) (val (item ,class))
(setf (slot-value item ',slot) val)
(changed item ',slot)))
(defmacro notifiers (class slots)
"Defines setf methods in (class) for all of (slots) which call the object's changed method."
`(progn
,@(loop for s in slots collecting `(notifier ,class ,s))))
(defmacro defclass-notifier-slots (class nslots slots)
"Defines a class with (nslots) giving a list of slots created with notifiers, and (slots) giving a list of slots created with regular accessors."
`(progn
(defclass ,class ()
( ,@(loop for s in nslots collecting `(,s :reader ,s))
,@(loop for s in slots collecting `(,s :accessor ,s))))
(notifiers ,class ,nslots)))
今書くことができる(defclass-notifier-slots foo (bar baz qux) (waldo))
直ちにクラス定義foo
正規スロットとwaldo
(仕様とマクロの第二部によって作成された(waldo :accessor waldo)
) 、 changed
メソッド(マクロの最初の部分(bar :reader bar)
、および呼び出されたnotifier
マクロによってセッターが定義されているゲッター)を呼び出すセッターで、slot bar
、 baz
、およびqux
を呼び出しnotifier
。
このように振る舞う複数のクラスを素早く定義することを可能にするだけでなく、反復せずに多数のプロパティを使用することで、コードの再利用という利点が得られます。マクロを使用し、それを使用するすべてのクラスの構造が変更されます。