逆引き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

マクロ展開の動作は

  1. 引数は評価されない。
  2. S 式を順番に評価し、いちばん最後の評価結果を再度評価して、その結果を返す。 なので,この場合には
(sum-macro x)
=> (cons '+ x)

と展開され,この式が評価される. すなわち (+ 1 2 3 4 5) が評価されることになる.

関数版の引数はクォートされているのに,マクロ版の引数はクォートされていないのは,マクロ展開では引数が評価されないため.

高階関数との違い

高階関数との大きな違いは、プログラムが実行される前コードを書き換えられる、という点にあります。

関数とマクロの使い分け

同じ内容を関数とマクロのどちらでも書けるような場合はどちらで書くのが正解か? 答えは関数.

マクロを使うべきタイミングは,マクロでしかできないことをするとき.

バッククォートとカンマ

::バッククォート()は括弧が入れ子になっていても全体にわたって作用する. テンプレートを使うとソースコードがマクロ展開形にほとんど等しくなるのでソースの読み書きが簡単になる. :,:バッククォートの中でも,"," がついている式は評価される. :,@: "," と同様に ",@" がついている式はバッククォートの中でも評価されるが,一番外側の括弧を外した形で展開される.

(defmacro nil! (var)
  `(setq ,var nil))
=> nil!

(nil! x)
= nil
x
=> nil

構文書き換えマクロの書き方

やりたいことをまずS式の形で書く

(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

便利なマクロいろいろ

一連の処理の前後に setup, cleanup を行う

(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

文字列をS式の中に直書きする

(++ 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))))))
⚠️ **GitHub.com Fallback** ⚠️