HOME Common Lisp 書き込む

4. 代入を簡略化するマクロ


4.1. はじめに

今回は値を代入するマクロについて解説します。値を代入するマクロとしては setf がありますが、タイプ量が多くなりがちな欠点があります。 ここでは、単に値を代入するマクロ、変数を操作してその結果をもともとの変数に 代入するマクロ、さらに一般的な代入マクロについて述べたいと思います。

4.2. 単に値を代入するマクロ

単に値を代入するだけなら、新しく定義するマクロの中で setf がそのまま使えます。 例として、引数を全て同じ値にセットする allfonlisp を取り上げます。 allf を使って、全ての引数を t または nil にセットする tfonlisp, nilfonlisp が定義できます。
(defmacro allf (val &rest args)                        ; (1)
  (let ((gval (gensym)))
    `(let ((,gval ,val))
       (setf ,@(mapcan #'(lambda (a) (list a gval))
                       args)))))

(defmacro nilf (&rest args) `(allf nil ,@args))        ; (2)

(defmacro tf (&rest args) `(allf t ,@args))            ; (3)
allf を展開すると次のようになります
(pme (allf the-same-value a b c d e))
(let ((#:G1 the-same-value))
   (setf a #:G1 b #:G1 c #:G1 d #:G1 e #:G1))

4.3. 変数を操作してその結果をもともとの変数に代入するマクロ

ある変数 (var) にある操作 (ope) を行ってそれをもとの変数 var に代入する操作は setf を使うと (4) のように書けます。 これを (5) の様にマクロ定義すると var を2回 評価することになりバグの元になります。例えば var (aref a (incf i)) だとすると。(5) のマクロは (6) の様に展開されることになり 値が正しくセットされません。(incf によって i の値が変化してしまう。)
(setf var (ope var))                             ; (4)

(defmacro setope (var)                           ; (5)
  `(setf ,var (ope ,var)))

(pme (setope (aref a (incf i))))                 ; (6) 
(setf (aref a (incf i))
       (ope (aref a (incf i))))
この種のマクロを書くマクロとして define-modify-macro が使えます。このマクロは次の3つの引数を取ります:
  1. 定義するマクロ名。
  2. 2番目以降の仮引数(一般化参照の場所(以下単に「場所」)は暗黙の第一引数になる) 仮引数には &optional, &rest も使用可。
  3. 「場所」の新しい値を返す関数。lambda 式、関数名のどちらも可。
以下に define-modify-macro を使って定義したマクロの例をあげます。C 言語風のオペレータが 定義できます。配列やハッシュ表の要素に変更を加えることも出来ます。
;;; C like operators
(define-modify-macro ++ () 1+)       ;  (7)
(define-modify-macro -- () 1-)       ;  (8)
(define-modify-macro += (var) +)     ;  (9)
(define-modify-macro -= (var) -)     ; (10)
(define-modify-macro *= (var) *)     ; (11)
(define-modify-macro /= (var) /)     ; (12)

;;; use C like operators
(setf a 100) [ctrl-j]
100
(++ a)[ctrl-j]
101
(-- a)[ctrl-j]
100
(/= a 5)[ctrl-j]
20
(*= a 10)[ctrl-j]
200
(+= a 200)[ctrl-j]
400
(-= a 300)[ctrl-j]
100

;;; apply to a hash table
(setf h (make-hash-table))[ctrl-j]
#<hashtable 46863060>
(setf (gethash 'k h) 100)[ctrl-j]
100
(++ (gethash 'k h))[ctrl-j]
101
(gethash 'k h)[ctrl-j]
101
t

;;; apply to an array
(setf ar #(10 20 30 40 50))[ctrl-j]
#(10 20 30 40 50)
(/= (aref ar 3) 20)[ctrl-j]
2
ar[ctrl-j]
#(10 20 30 2 50)
define-modify-macro の第3引数に lambda 式を書く場合は次のようにします。 lambda 式の最初の仮引数は「場所」を指定します。 (13) は複素数を複素平面上で phi だけ回転させて元の「場所」にセットするマクロです。
(define-modify-macro rotc (phi)                            ; (13)
		     (lambda (c0 phi)
		       (* c0 (exp (* #C(0 1) phi)))))

;;; use rotc
(setq a 1)[ctrl-j]
1
(rotc a (/ pi 4))[ctrl-j]
#C(0.7071067811865476d0 0.7071067811865475d0)
(rotc a (/ pi 4))[ctrl-j]
#C(2.220446049250313d-16 1.0d0)
a[ctrl-j]
#C(2.220446049250313d-16 1.0d0)
define-modify-macro で定義できるのは 「場所」が特定でき、変更された値を返すマクロのみです。 従って、push や pop はdefine-modify-macro では定義できません。 (push は変更するのが特定の「場所」ではなく、 pop は返り値が変更されたオブジェクトではないから。) push や pop を定義するには次の節で述べる get-setf-expansion を使います。

4.4. さらに一般的な代入マクロ

ある「場所」の値を参照したり、値をセットしたりする方法は get-setf-expansion (古い処理系では get-setf-method)で得られます。 ちなみに、xyzzy では get-setf-method のみが使えます。また、clisp では 両方が使えます。ANSI Common Lisp では get-setf-expansion が標準ですので、 (14) 式の様に xyzzy でも get-setf-expansion を定義して siteinit.l か .xyzzy に 書いておきましょう。

get-setf-expansion は「場所」の値を参照したり代入したりする方法を返します。処理系によって これらの方法は違うので、可搬性のあるプログラムを書くためには、実装に依存する方法を 直接使うことなく、get-setf-expansion の返り値を使うべきです。 式 (15)--(18) に xyzzy と clisp で 配列とハッシュ表の場合の get-setf-expansion の返り値を示します。 (15)--(18) にあるように返り値は実装により異なります。 get-setf-expansion は 次の5つの値を返します:

  1. 引数で渡された値を格納する変数のリスト。(以下 vars)
  2. 「場所」を特定するために引数で与えた値のリスト。(以下 argvs
  3. 新しい値を「場所」にセットするときに使う変数のリスト。(以下 val
  4. 「場所」に新しい値をセットする関数。(以下 setter
  5. 「場所」の値を参照する関数。(以下 accessor
これを使うと、push, pop は (19), (20) の様に書けます。 また、要素数 n の部分リストを取り出す マクロ popnonlisp(21) の様に 書けます。

一般にこの種のマクロの書き方は以下の通りです。具体的なやり方は (19)--(21) を 見てください。

  1. argvs の値を vars に代入する。
  2. val にあたらしい値を代入する。
  3. setter を使って「場所」の値を更新する。
  4. 返り値があればそれを返す。
;;; should be defined in siteinit.l or .xyzzy
(defmacro get-setf-expansion (&rest argvs)                  ; (14)
  `(get-setf-method ,@argvs))

;;; xyzzy array
(get-setf-expansion '(aref a i j))[ctrl-j]                  ; (15)
(#:G1 #:G2 #:G3)
(a i j)
(#:G4)
(system:*aset #:G1 #:G4 #:G2 #:G3)
(aref #:G1 #:G2 #:G3)

;;; xyzzy hash table
(get-setf-expansion '(gethash key *hash*))[ctrl-j]          ; (16)
(#:G5 #:G6)
(key *hash*)
(#:G7)
(system:*puthash #:G5 #:G6 #:G7)
(gethash #:G5 #:G6)

;;; clisp array
[21]> (get-setf-expansion '(aref a i j))[Return]            ; (17)
(#:G2091 #:G2092 #:G2093) ;
(A I J) ;
(#:G2094) ;
(SYSTEM::STORE #:G2091 #:G2092 #:G2093 #:G2094) ;
(AREF #:G2091 #:G2092 #:G2093)

;;; clisp hash table
[22]> (get-setf-expansion '(gethash key *hash*))[Return]    ; (18)
(#:G2095 #:G2096) ;
(KEY *HASH*) ;
(#:G2097) ;
(SYSTEM::PUTHASH #:G2095 #:G2096 #:G2097) ;
(GETHASH #:G2095 #:G2096)

;;; our push
(defmacro our-push (obj place)                              ; (19)
    (multiple-value-bind (vars argvs val setter accessor)
      (get-setf-expansion place)
      `(let ,(mapcar #'list vars argvs)
	 (let ((,@val (cons ,obj ,accessor)))
	   ,setter))))

;;; our pop
(defmacro our-pop (place)                                   ; (20)
  (let ((gval (gensym)))
    (multiple-value-bind (vars argvs val setter accessor)
	(get-setf-expansion place)
      `(let ,(mapcar #'list vars argvs)
	 (let ((,gval (car ,accessor))
               (,@val (cdr ,accessor)))
	   ,setter
	   ,gval)))))

;;;;
(defmacro popn (n place)                                    ; (21) 
  (let ((gval (gensym)) (gn (gensym)))
    (multiple-value-bind (vars argvs val setter accessor)
	(get-setf-expansion place)
      `(let ,(mapcar #'list vars argvs)
         (let ((,gn ,n))
           (let ((,gval (subseq ,accessor 0 ,gn))
                 (,@val (nthcdr ,gn ,accessor)))
	     ,setter
	     ,gval))))))

3. 終わりに

今回は値を代入するためのマクロについて述べてみました。 次回は関数を定義するマクロについて解説します。

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