初めての人のためのLISP - 第10講 変態プログラム

初めての人のためのLISP[増補改訂版]のメモです。
ついにマクロ。
最初:初めての人のためのLISP (第1講-第5講) - もうカツ丼でいいよな

置き換えてから評価する

(defun first (x) (car x))

という関数があったとき

(first (list 1 2 3))

本来なら関数firstはxにリスト(1 2 3)を束縛した後に(car x)を評価するが、そうではなく上記関数を

(car (list 1 2 3))

と置き換えてから評価したいとする。この目的のため、古いLispにはdefsubstと呼ばれるものがあった。

(defsubst first (x) (car x))

のように使う。
defsubstは呼び出し時には引数を評価せず、置き換え後に評価する。呼び出し時に評価するか否かの違いは評価により値の変化するS式を引数とする場合(ifなど)に重要となる。

S式からS式への変換 subst

(subst x y z)
  • zの中のyをxに置き換えた新しいリストを返す(substitute x for all y in z)
  • サブスト

定義1(全ての要素をコピーする)

(defun subst (new old tree)
       (cond ((eq old tree) new) ; 葉(アトム)がoldと等しければnewに置き換える
             ((atom tree) tree)  ; そうでなければ葉をそのまま返す
             (t (cons (subst new old (car tree)) ; 葉でなければcarと
                      (subst new old (cdr tree)) )))) ; cdr方向に再帰

リストを木構造としてとらえている。リストのcarが根。
近頃のLispでは書き換えのおこらなかった部分木についてはコピーしない。
定義2(書き換えの起こらなかった部分木はコピーしない)

(defun subst (new old tree)
       (cond ((eq old tree) new)
             ((atom tree) tree)
             (t (let  ((a (subst new old (car tree))) ; carのsubst結果をaに
                       (d (subst new old (cdr tree))) ) ;cdrのsubst結果をdに
                      (cond ((and (eq a (car tree)) ; substの前後でcarも
                                  (eq d (cdr tree)) ) ;cdrも変化がなければ
                             tree) ; treeをそのまま帰す
                            (t (cons a d)) )))))) ; 変化していたら新しいリストを作成

letはprogの4つの機能(一時的な変数、go、return、逐次評価)のうち、変数の用意と逐次評価のみを取り出したもの。

(let ((変数1 初期値1)
      (変数2 初期値2)
      ...
      (変数n 初期値n))
      式1 式2 ... 式m)

変数1..nに初期値1..nがセットされ、式1..mが順に評価されたのちに式mの値が帰る。

一括して置き換えるsublis

(defun sublis (alist tree)
       (let ((pair (assoc tree alist))) ; 葉(アトム)の要素がalistから見つかればpairにドット対がセットされる
            (cond (pair (cdr pair)) ; 葉が上でヒットしていたらドット対のcdrを返す
                  ((atom tree) tree)
                  (t (let ((a (sublis alist (car tree)))
                           (d (sublis alist (cdr tree))) )
                          (cond ((and (eq a (car tree))
                                      (eq d (cdr tree)) )
                                 tree )
                                (t (cons a b)) ))))))
  • サブリス
  • 引数にalistを与え、一括して置き換え
  • assocでヒットするか否かにより、与えられたtreeが置き換え対象のアトムか否かを判断している。

if

if p then x else y

(defun if (p x y) (cond (p x) (t y)))

この場合、呼び出し時にxもyも評価されてしまう。setqを含むS式を渡す場合、想像とは違う動作になる可能性が高い。
もしdefsubstが使えるなら次のように定義することで目的を果たせる。

(defsubst if (p x y) (cond (p x) (t y)))

これは

(if flag a b)

としてifを呼びだすと、

(sublis '((p . flag)
          (x . a)
          (y . b))
        '(cond (p x) (t y)))

という変換をした後に変換後のS式を評価する。

&rest

condのような特殊形式やlistのような関数は不定個の引数を受け取る。不定個の引数を受け取るには&restを使う。

(defun fun (x y &rest z))

このとき、xとyに続く3番目以降の引数は全てリストにまとめられてzに入る。

マクロ

マクロはdefmacroにより定義する。マクロではまず本体を評価して新しい式を生成し(マクロ展開)、得られた値を呼び出した場所で改めて評価する。また、呼び出し時には引数を評価しない。

(defmacro first (x) (list 'car x))

(first (list a b c))

として呼びだすとまずマクロの本体部分が評価され、

(car (list a b c))

が得られる。そしてこれが呼び出した場所で評価される。

バッククォート

バッククォートは基本的にはクォートと変わらない。

'x ;=> x
`x ;=> x
'(a b c) ;=> (a b c)
`(a b c) ;=> (a b c)

ただし、リストに対して用いられた場合、カンマ(,)を用いることでクオートの効果を打ち消すことができる。

(setq b 3)
'(a b c) ;=> (a b c)
`(a ,b c) ;=> (a 3 c)
(list 'a b 'c) ;=> (a 3 c) クォート付きで呼び出したlistからクオートを外すのと同じ

バッククォート記法を使うと、マクロを見やすく記述できる場合がある。

(defmacro first (x) (list 'car x))
(defmacro first (x) `(car ,x))

リストのつなぎ込み

バッククォート中で,@に続くものがリストであるとき、リストは周囲の要素と同じレベルにつなぎ込まれる。

(setq x '(2 3))
`(1 ,@x 4) ;=> (1 2 3 4)

popとpushの定義

(defmacro pop (x) '(prog1 (car ,x) (setq ,x (cdr ,x))))
(defmacro push (y x) '(setq ,x (cons ,y ,x)))
(setq x '(1 2 3))
(pop x) ;=> 1
x ;=> (2 3)
(push 5 x) ;=> (5 2 3)
x ;=> (5 2 3)