HOME Common Lisp 書き込む

1. テンプレートの使い方と展開形の確認


1.1. はじめに

LISP のプログラムは LISP の主要なデータ型であるリストで表現されます。 すなわち、プログラムとデータの区別がほとんど無い、というのが LISP の特徴の 1つです。このことから、 "プログラムを書くプログラム" を書くことが出来ます。この機能をマクロと言います。 C などの言語にもマクロはありますが、 LISP のマクロは他の言語のものと比べて 著しく強力です。

LISP には構文がほとんど無く、一般に "構文" と呼ばれている do, while などは実はマクロです。 マクロは "構文定義 + α" の働きがあります。

マクロは 2段階にわたって評価されます。最初の段階は、マクロ定義に従ってマクロを展開することです。 2番目の段階は展開された式を評価し値や副作用を得ることです。 マクロがトップレベルから呼ばれるとこの2つの操作が続けて行われます。 一方、ソースコード内に書かれたマクロは、ソースをコンパイルするとき、 ソースがコンパイラーに渡る前に、マクロの展開形がそのマクロか呼ばれた場所に貼り付けられます。 これによって、関数では表せない操作や、コンパイル時に処理したい操作をマクロとして定義することにより ソースコードをすっきりと書くことが出来ます。 つまり、マクロは LISP をコンパイル言語として使用するときに威力を発揮します

マクロは非常に強力で便利な機能ですが、一般の LISP の教科書ではあまり詳しく書かれていません。 これらの教科書を読んで使えるマクロを書くことは不可能です。使えるマクロを書くためには On Lisp を読む必要があります。また、 現在進行中ながら日本語版 もあります。 両方ともフリーでダウンロードできます。筆者も大変勉強になりました。この文書でも必要に迫られて On Lisp から借用したマクロを多数紹介しています。On Lisp から引用したマクロには onlisp をつけて出典を明らかにしています。

ここでは筆者が On Lisp から学んだことをなるべく筆者が書いたマクロを実例にとりながら 解説していこうと思ってます。この講座の目的は小、中規模の実用的なマクロの書き方を分かりやすく 解説することです。

1.2. 簡単なマクロの書き方

ここでは xyzzy を使ってマクロを書き、マクロ展開形を確かめるという前提で 話を進めます。xyzzy は一応エディタですが、ほぼ、Common Lisp に準拠しているので、 xyzzy で書いたマクロは少数の例外を除き clisp でも同じ動作をします。 xyzzy でマクロや関数を試作してチェックするには xyzzy を立ち上げたときにまず現れる *scratch* バッファを使います。*scratch* 上に S 式を書き、S 式の最後の部分にカーソルをあわせて Ctrl-j を押すとその S 式の評価 が行われます。

(+ 1 1) [ctrl-j]
2
マクロは "defmacro" を使って定義します。 変数を nil にセットするマクロ nil!onlisp は以下の様に定義されます。
(defmacro nil! (var)
  (list 'setf var nil))                 ;(1)
ソースコード上に (nil! a) と書いてある部分はコンパイル時に (setf a nil) に展開され、コンパイラーにわたります。

1.2. テンプレート

上の例では簡単なので普通に list を使っても大して複雑ではありませんが、 少し複雑なマクロを書くときに list を使っていると破綻します。複雑なマクロを 書くためにテンプレートが用意されています。テンプレートはバッククォート(`)、コンマ(,)、コンマ/アト(,@) からなります。バッククォートはクォートと同じように内部の式を評価しない様にします。 ただし、コンマ(,)、コンマ/アト(,@)のついている式は評価します。テンプレートを使うと 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 では両者はまったく同じです。

1.3. 展開形の確認

マクロが正しく書けているか調べるためにその展開形を確認する必要があります。 展開形を確認するには macroexpand-1 を使います。clisp では pprint を使うことによって 展開形は読みやすいように改行され、字下げされますが、xyzzy には残念ながら pprint がありません。 そこで pprint と似た動作をするマクロ pme (Print Macro Expansion) を作ってみました。 ここ にあるソースを siteinit.l に貼り付けてバイトコンパイルして、 ダンプファイルを再作成してください。siteinit.l についての詳しいことは xyzzy 日記 (2) 置き場所を決める を見てください。

*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)

1.4. マップ関数 (mapcar, mapcan, mapc) とテンプレート

コンマ(,)、コンマ/アト(,@)はマップ関数と組み合わせると威力を発揮します。 mapcar は関数とリストを引数にとり、関数によって加工されたリストを返します。 引数として複数のリストを取ることが出来ます。マクロの作成には主に mapcar を使います。 mapcan は平坦なリストを返しますが、破壊的な操作ですので使用には注意が必要です (ちなみに、xyzzy でも clisp でも引数は破壊されません)。mapc は加工されたリストを返さないので (最後の引数をそのまま返す)、リストをマクロ作成にはあまり利用されません。
(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))

1.5. 終わりに

今回はテンプレートの使い方と展開形の確認について解説しました。 次回は実用的なマクロで問題になる変数の捕捉と多重評価 について解説します。

不明な点、不正確な点などがありましたら紫藤まで お知らせいただけたら幸いです。(shido_takafumi@ybb.ne.jp)