with-slots

Common Lisp にある with-slots を Gauche でも使ってみる。次のクラスがあったとき、

(define-class <person> ()
  ((name :init-keyword :name)
   (gender :init-keyword :gender)
   (age :init-keyword :age)))

こんなふうに使えるようにする。

;; Example-1
(let ((hoge (make <person> :name "Hoge" :gender 'male :age 20)))
  (with-slots (name gender age) hoge
    (format #t "~a: ~a ~d~%" name gender age)))

まず define-syntax を使ってマクロを書いてみた。

(define-syntax with-slots
  (syntax-rules ()
    ((_ () obj body ...)
     (begin body ...))
    ((_ (slot1 slot2 ...) obj body ...)
     (let ((slot1 (slot-ref obj 'slot1)))
       (with-slots (slot2 ...) obj body ...)))))

Example-1 を評価した限り、うまく動いているように見える。しかし次のような場合はどうか。

;; Example-2
(with-slots (name gender age)
    (make <person> :name "Hoge" :gender 'male :age 20)
  (format #t "~a: ~a ~d~%" name gender age))

これだとスロットが展開されるたびに インスタンスが作られてしまう。 の初期化でメッセージを出すようにしてみる。

(define-method initialize ((p <person>) initargs)
  (next-method)
  (format #t "[~a] initialized.~%" (slot-ref p 'name)))

with-slots で指定した数だけ initialize が呼び出されるのがわかる。

たぶん、define-syntax でもこういう場合の回避方法はあるんだろうが、今のところ不明なので define-macro でマクロを書くことにした(追記: shiro さんのコメント参照)。こちらは Common Lisp のマクロの書き方がそのまま参考になる。

(define-macro (with-slots slots obj . body)
  (let ((gobj (gensym)))
    `(let ((,gobj ,obj))
       (let ,(map (lambda (slot)
                    `(,slot (slot-ref ,gobj ',slot)))
                  slots)
         ,@body))))

今度は最初に一回だけ obj を評価して展開するので Example-2 でも問題ない。

Common Lisp の with-slots は束縛するシンボルも指定できる。

(with-slots ((name1 name) (age1 age)) person1
  (with-slots ((name2 name) (age2 age)) person2
    ;; ...
    ))

でも今回は省略。