逆引きxyzzy lisp(マクロ) - circleratio/xyzzy GitHub Wiki
Lispのコードは大きく3回にわけて実行されます。
そのため、プログラムを実行する前にコードを書き換えることが可能. これがマクロ展開.
これは関数のインライン展開と同じ.
(defmacro sum-macro (x)
(cons '+ x))
(sum-macro (1 2 3 4 5))
=> 15
同じ処理を関数で書くと次のようになる.
(defun sum-func (x)
(apply #'+ x))
(sum-func '(1 2 3 4 5))
=> 15
マクロ展開の動作は
- 引数は評価されない。
- S 式を順番に評価し、いちばん最後の評価結果を再度評価して、その結果を返す。 なので,この場合には
(sum-macro x)
=> (cons '+ x)
と展開され,この式が評価される. すなわち (+ 1 2 3 4 5) が評価されることになる.
関数版の引数はクォートされているのに,マクロ版の引数はクォートされていないのは,マクロ展開では引数が評価されないため.
高階関数との大きな違いは、プログラムが実行される前コードを書き換えられる、という点にあります。
同じ内容を関数とマクロのどちらでも書けるような場合はどちらで書くのが正解か? 答えは関数.
マクロを使うべきタイミングは,マクロでしかできないことをするとき.
::バッククォート(
)は括弧が入れ子になっていても全体にわたって作用する. テンプレートを使うとソースコードがマクロ展開形にほとんど等しくなるのでソースの読み書きが簡単になる.
:,:バッククォートの中でも,"," がついている式は評価される.
:,@: "," と同様に ",@" がついている式はバッククォートの中でも評価されるが,一番外側の括弧を外した形で展開される.
(defmacro nil! (var)
`(setq ,var nil))
=> nil!
(nil! x)
= nil
x
=> nil
(for ((i 2) (j 2) (k 2))
body)
=>
(dotimes (i 2)
(dotimes (j 2)
(dotimes (k 2)
body)))
(defun make-doforms (bindforms body)
(if (null bindforms) body
(list 'dotimes
(car bindforms)
(make-doforms (cdr bindforms) body))))
(defmacro for ((&rest args) body)
(make-doforms args body))
(macroexpand-1 '(for ((i 2) (j 2) (k 2)) (print i)))
=> (dotimes (i 2) (dotimes (j 2) (dotimes (k 2) (print i))))
t
(for ((i 2) (j 2))
(print (+ (* i 10) j)))
=> 0
1
10
11
nil
(M (f1) (f2) (f3)) を呼ぶと
(progn
(setup)
(f1)
(f2)
(f3)
(cleanup))
を実行したい場合は次のようにする.
(defmacro M (&rest args)
`(progn
(setup)
,@args
(cleanup)))
(macroexpand-1 '(M (f1) (f2) (f3)))
=> (progn (setup) (f1) (f2) (f3) (cleanup))
t
(++ 1万 2万)
=> 3万
といった計算をしたい場合,"1万" はシンボル扱いされてしまうので,次のようなエラーになる.
(++ 1万 2万)
=> 変数が定義されていません: 1万
マクロを使うと,評価される前に処理を加えることができるので,こういった表記も可能になる.
計算を実行する場合.
(defmacro direct-string (func &rest body)
(cons
func
(mapcar (lambda (x)
(cond ((symbolp x) (string x))
((stringp x) x)
((numberp x) (format nil "~A" x))))
body)))
(direct-string concat 10x 20x "10,00" 20)
=> "10x20x10,0020"
関数を作る場合.
(defmacro direct-string (func &rest body)
`(cons
,func
(mapcar (lambda (x)
(cond ((symbolp x) (string x))
((stringp x) x)
((numberp x) (format nil "~A" x))))
',body)))
(direct-string #'concat 10x 20x "10,00" 20)
=> (#<lexical-closure: concat> "10x" "20x" "10,00" "20")
ただし,マクロ内でも特殊扱いされる括弧やカンマといった文字は使えない.この場合はあきらめてクォートすること.
マクロの展開形を確認する macroexpand-1 は改行や字下げをしてくれない。 clisp では pprint がこれをやってくれるが、xyzzy lisp にはないので、下記マクロを使うとよい(http://www.shido.info/lisp/add2li.l.txt)。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; script add to lisp-inteactive-mode
;;; by T.Shido; July 18, 2004
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; show macro expansion, Print Macor Expansion
(defmacro pme (mac)
`(pprint-1 (macroexpand-1 ',mac)))
(defun pprint-1 (s0)
(pp-loop (format nil "~S" s0)))
(defun pp-loop (str0 &optional then)
(if (and then (eql 0 (string-match " *[^ (]+" str0)))
(let ((pe (match-end 0)))
(insert (substring str0 0 pe))
(lisp-newline-and-indent)
(pp-loop (substring str0 pe) nil))
(progn
(string-match ")+\\|( *cond +\\|case +[^ (]+" str0)
(let* ((px (match-end 0))
(str1 (substring str0 0 px))
(pif0 (string-match "\\( *( *if +\\)\\|\\( *( *if +[^ (]+\\)" str1))
(pif1 (match-end 2))
(p1 (or pif1 px)))
(if (and pif0 (< 0 pif0))
(progn
(insert (substring str0 0 pif0))
(lisp-newline-and-indent)
(insert (substring str0 pif0 p1)))
(insert (substring str0 0 p1)))
(lisp-newline-and-indent)
(if (< p1 (length str0))
(pp-loop (substring str0 p1) pif0))))))