HOME | Common Lisp | 2. 多重評価と変数捕捉 | 書き込む |
LISP には構文がほとんど無く、一般に "構文" と呼ばれている do, while などは実はマクロです。 マクロは "構文定義 + α" の働きがあります。
マクロは 2段階にわたって評価されます。最初の段階は、マクロ定義に従ってマクロを展開することです。 2番目の段階は展開された式を評価し値や副作用を得ることです。 マクロがトップレベルから呼ばれるとこの2つの操作が続けて行われます。 一方、ソースコード内に書かれたマクロは、ソースをコンパイルするとき、 ソースがコンパイラーに渡る前に、マクロの展開形がそのマクロか呼ばれた場所に貼り付けられます。 これによって、関数では表せない操作や、コンパイル時に処理したい操作をマクロとして定義することにより ソースコードをすっきりと書くことが出来ます。 つまり、マクロは LISP をコンパイル言語として使用するときに威力を発揮します。
マクロは非常に強力で便利な機能ですが、一般の LISP の教科書ではあまり詳しく書かれていません。 これらの教科書を読んで使えるマクロを書くことは不可能です。使えるマクロを書くためには On Lisp を読む必要があります。また、 現在進行中ながら日本語版 もあります。 両方ともフリーでダウンロードできます。筆者も大変勉強になりました。この文書でも必要に迫られて On Lisp から借用したマクロを多数紹介しています。On Lisp から引用したマクロには onlisp をつけて出典を明らかにしています。
ここでは筆者が On Lisp から学んだことをなるべく筆者が書いたマクロを実例にとりながら 解説していこうと思ってます。この講座の目的は小、中規模の実用的なマクロの書き方を分かりやすく 解説することです。
(+ 1 1) [ctrl-j]
2
マクロは "defmacro" を使って定義します。
変数を nil にセットするマクロ
nil!onlisp は以下の様に定義されます。
(defmacro nil! (var)
(list 'setf var nil)) ;(1)
ソースコード上に
(nil! a)
と書いてある部分はコンパイル時に
(setf a nil)
に展開され、コンパイラーにわたります。
(defmacro nil! (var)
`(setf ,var nil)) ;(2)
バッククォート(`)は括弧が入れ子になっていても全体にわたって作用します。
テンプレートを使うとソースコードがマクロ展開形にほとんど等しくなるのでソースの
読み書きがやさしくなります。コンマ/アト(,@)の働きはコンマ(,)と似ていますが、一番外側の括弧をはずして展開されます。 コンマ/アト(,@) を使う簡単な例は省略形の定義です。例えば、 multiple-value-bind の省略形 mvbindonlisp を定義します。
(defmacro mvbind (&rest argvs) `(multiple-value-bind ,@argvs))簡単に省略形が定義できました。 defmacro でも defun と同様に &rest, &body, &optional パラメータが使えます。 &body は &rest と同じ意味です注。
また、コンマ、コンマ/アト は、シンボルではなく、S 式に付けることに注意してください。例えば平均値を求める マクロ aveonlisp では コンマは (length argvs) についています。これはコンパイル時に (length argvs) を評価してその値を 使うことを意味しています。つまり、実行時の計算が減り、プログラムが高速に実行できるようになります。
(defmacro ave (&rest argvs)
`(/ (+ ,@argvs) ,(length argvs))) ;(3)
注: &body には整形表示効果があります。つまり、clisp では pprint によって、自動的に
改行され、字下げされます。pprint の無い xyzzy では両者はまったく同じです。
*scratch* 上で ave を定義して、展開形を確認してみましょう。 まず、(3) のコードを *scratch* にコピーして、最後の閉じ括弧にカーソルをあわせて Ctrl-j を押します。すると 'ave' というエコーがあり、マクロ ave が定義されました。 それでは展開形を確かめて見ましょう。*scratch* に
(pme (ave a b c d))と書き、Ctrl-j を押すと、
(/ (+ a b c d) 4)と表示されます。(length argvs) が '4' に置き換わったのが分かります。macroexpand-1 をそのまま使うと 改行と字下げがされないので展開形が長くなるマクロを読むときは不便です。
(macroexpand-1 '(ave a b c d)) [ctrl-j]
(/ (+ a b c d) 4)
(mapcar #'(lambda (x y) (+ x y)) '(1 2 3 4) '(10 20 30 40)) [ctrl-j] (11 22 33 44) (mapcar #'(lambda (x y) `(,x ,y)) '(foo hoge bar baz) '(red white blue green)) [ctrl-j] ((foo red) (hoge white) (bar blue) (baz green)) (mapcan #'(lambda (x y) `(,x ,y)) '(foo hoge bar baz) '(red white blue green)) [ctrl-j] (foo red hoge white bar blue baz green)それでは、マップ関数を使って nil! を改良してみましょう。setf は複数の引数が取れるので、 nil! も複数の引数が取れるようにしましょう。
(defmacro nil! (&rest argvs) `(setf ,@(mapcan #'(lambda (x) (list x nil)) argvs))) [ctrl-j] nil! (pme (nil! a b c)) [ctrl-j] (setf a nil b nil c nil) (nil! a b c) [ctrl-j] nil a [ctrl-j] nil b [ctrl-j] nil c [ctrl-j] nil最後に、mapcar を使ったマクロ with-gensymsonlisp と mvset を紹介します。
with-gensyms は多量の gensym を作成するのに利用します。次回に述べるように 変数の捕捉を避けるため、展開形内部に現れる変数は他のシンボルと絶対に衝突しない gensym で生成されたシンボルを使う必要があります。一方、mvset は多値を返す関数の返り値をセットするのに使います。
;;; create gensyms (defmacro with-gensyms (syms &body body) `(let ,(mapcar #'(lambda (s) `(,s (gensym))) syms) ,@body)) ;;; multiple-value set (defmacro mvset(parms func) (let ((genparms (mapcar #'(lambda(x) (gensym (symbol-name x))) parms))) `(multiple-value-bind ,genparms ,func (setf ,@(mapcan #'(lambda(x y) (list x y)) parms genparms))))) ;;; macro expansions (pme (with-gensyms (a b c d) `(let ((,a 1) (,b 2) (,c 3) (,d 4)) (+ ,a ,b ,c ,d)))) [ctrl-j] (let ((a (gensym)) (b (gensym)) (c (gensym)) (d (gensym))) (list 'let (list (cons a '(1)) (cons b '(2)) (cons c '(3)) (cons d '(4))) (list '+ a b c d))) (pme (mvset (a b) (floor 10 3)))[ctrl-j] (multiple-value-bind (#:a3 #:b4) (floor 10 3) (setf a #:a3 b #:b4))
不明な点、不正確な点などがありましたら紫藤まで お知らせいただけたら幸いです。(shido_takafumi@ybb.ne.jp)
HOME | Common Lisp | 2. 多重評価と変数捕捉 | 書き込む |