プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(42)

更新をさぼってしまいました。
完全に迷走していたこのブログ、冷静になって考えなおし、
色々手を出すのは、ちゃんと基礎を学んでからにしようと思いました。

というわけで「実践Common Lisp」、8章です。マクロを実際に書いてみます。

■マクロを書くときのステップ(おさらい)
1.マクロ呼び出しのサンプルを書き、その展開形を書く。あるいはその逆。
2.展開形を生成するコードを書く。
3.マクロによる抽象化に漏れがないことを確認する。

■ステップ1
素数に対して反復するdo-primesというマクロを例に書きます。
準備として、引数が素数かどうか判別する関数と、引数以上の素数を返す関数を用意します。

CL-USER> (defun primep (number)
           (when (> number 1)
             (loop for fac from 2 to (isqrt number) ;2からnumberの平方根以下の整数の間でfac
                  never (zerop (mod number fac))))) ;2からfacの間で割り切れなければ素数
PRIMEP
CL-USER> (defun next-prime (number)
           (loop for n from number
                when (primep n) return n)) ;number以上の素数が出るまでループ
NEXT-PRIME
CL-USER>

素数を求める関数は力技ですね。

準備ができたので、マクロを書くステップその1です。
こんな風に書ければいいのに、と思いついたとする例です。

(do-prime (p 0 19)
          (format t "~d " p))

0以上、19以下の素数それぞれに対してループ本体(format)が一回実行されて欲しいのですね。
フォームはDOLISTやDOTIMESと同じですが、奇をてらわず、既存のもののパターンに沿ったほうが
使いやすいものだそうです。えてしてそういうものですね。

さて、do-primeが無ければ、doを使って書くしかなく、つまりそれが展開形です。

(do ((p (next-prime 0) (next-prime (1+ p)))) ;ループ内変数の初期値とそれ以降の設定
    ((> p 19))                                ;終了条件の設定
  (format t "~d " p))                       ;処理本体

■ステップ2
マクロに渡される引数は、そのマクロ呼び出しのソースコードを表わすLispオブジェクトです。
つまり、展開形を得るのに必要なオブジェクトのパーツを全て抽出するのが第一歩です。

引数をそのままテンプレートに当てはめるだけのマクロならこれは簡単で、
別々の引数がそれぞれ適切なパラメータに保持されるようにすればいいのですが、
今回はそれでは不十分です。

do-prime呼び出しの最初の引数は、(p 0 19)というようなリストであり、
展開形にはそんなリストがありません。ばらばらに配置されます。

do-primeでは、2つの引数を取るようにして、1つめが上のリストを、
2つめがループ本体のフォームを保持するようにするのがいいようです。
展開したときはリストを本体の中でバラバラにしてやります。

(defmacro do-primes (var-and-range &rest body)
  (let ((var (first var-and-range))
        (start (second var-and-range))
        (end (third var-and-range)))
    '(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
         ((> ,var ,end))
       ,@body)))

まずは与えられたパラメータのうち、1つ目のものを分解してそれぞれを別の変数にし、
それを本体に埋め込んでいます。
,@は、それに続くリストを外側のリストと繋ぎあわせるアンクォートの亜種なのですね。

CL-USER> `(list a b ,@(list 1 2))
(LIST A B 1 2)
CL-USER> `(list a b ,(list 1 2))
(LIST A B (1 2))
CL-USER>

ふむふむ。

さて、上のものではリストを分解してフォームに埋め込んでいましたが、
マクロパラメータのリストは分配パラメータといい、フォームの構造をばらして変数に格納してくれるそう。
分配パラメータリスト内には、普通のパラメータ名だけでなく、
パラメータリストを入れ子にして指定できるとのこと。

他にもマクロのパラメータリストには特別な機能があって、&restと同じ意味で&bodyが使えると。
同じ意味ならわざわざある必要、と思いますが、たいていの開発環境では&bodyがあると
マクロで使うインデントを調整してくれるそうです。
&bodyはマクロ本体のフォームに使われることがよくあるということです。
さて、今のことを踏まえて、もう一度。そろそろちゃんと実行します。

CL-USER> (defmacro do-primes ((var start end) &body body)
           `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
                ((> ,var ,end))
              ,@body))
DO-PRIMES
CL-USER> 

すごい明瞭な書き方ができるようになりました。
分配パラメータリストはコードを綺麗にするだけではなく、エラーチェックも追加してくれるそう。
後者の方法でdo-primesを実装すれば、最初の引数が3要素のリストかどうかLispに確認してもらえます。
あと開発環境がマクロ呼び出しのシンタックスを教えてくれるという地味に大事なメリットも。

分配パラメータには&optional,&key,&restも含めることができるということです。

■展開形の生成
do-primesのようなシンプルなマクロにはバッククォートのシンタックスがぴったりだそうです。
バッククォートは便利だという話が続きます...。

このマクロが正しく動くか確認しなくてはいけません。
今のdo-primesには漏れがあるそうですが、それは置いといて使ってみます。

CL-USER> (do-primes (p 0 19)
           (format t "~d " p))
2 3 5 7 11 13 17 19 
NIL
CL-USER>

素数が表示されています。少なくともこの範囲では正しく動いているようです。

もう一つ、特定の呼び出しにおけるマクロの展開結果を直接チェックする方法もあるそう。
それが関数macroexpand-1というらしいので、使ってみます。

CL-USER> (macroexpand-1 '(do-primes (p 0 19) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P)))) ((> P 19)) (FORMAT T "~d " P))
T
CL-USER>

うまいこと考えていた展開形になってくれているようです。

今日はここまで。明日は漏れをふさぎます。