SBCLとCCLの違い:defstructのincludeにおけるスロット名の比較方法

Common Lispの構造体定義マクロdefstructでは、:includeで別の構造体のスロット名やその初期値を引継ぐことができます。このとき、下記の(a 100)のように初期値を上書きできます。

CL-USER> (defstruct parent (a 10) (b 20))
PARENT
CL-USER> (defstruct (child (:include parent (a 100))) c)
CHILD
CL-USER> (make-child)
#S(CHILD :A 100 :B 20 :C NIL)

:include内で存在しないスロット名を指定すると当然エラーになるわけですが、このときのスロット名の比較(存在するかを判定するための)の仕方がSBCLとClozure CL (CCL) *1で異なるようだったのでメモ。

gist.github.com

このRoswellスクリプトを走らせると、SBCLではエラーなく動きますが、

#S(TEST-STRUCT2 :SLOT1 100 :SLOT2 NIL)

CCLではslot1なんて知らんと言われてエラーになります(CLISPもこのパターンでした)。

Error: TEST-STRUCT has no SLOT1 slot, in (:INCLUDE TEST-STRUCT (SLOT1 100))

CCLで動かすためには、初期値を上書きする部分でpack-ainternされたslot1シンボルを指定する必要があります(pack-aの定義でslot1もexportするなど)。なお、exportしておく分にはSBCLでも問題なく動くので、必要な場合はexportしておくのが正しいと思います。

includeにおいて、SBCLではパッケージに依存しない形でスロット名を比較しており、CCLではパッケージに依存した形で比較しているようです。…で終わるのも悲しいので、それぞれの該当部分のソースもほんの少し見てみます。

まずSBCLのソースです。手元にあったバージョン1.2.15を見てみました。エラーメッセージから該当箇所は簡単に見つかります。:test #'string=と比較方法を指定しているので、確かにパッケージを含まないシンボル名のみで比較しているようです。

;; L768~@src/code/defstruct.lisp
(defun frob-dd-inclusion-stuff (dd)
;; ...(略)...
      (flet ((included-slot-name (slot-desc)
               (if (atom slot-desc) slot-desc (car slot-desc))))
        (mapl (lambda (slots &aux (name (included-slot-name (car slots))))
                (unless (find name (dd-slots included-structure)
                              :test #'string= :key #'dsd-name)
                  (error 'simple-program-error
                         :format-control "slot name ~S not present in included structure"
                         :format-arguments (list name)))
;; ...(略)...

次はCCLです。バージョンは1.9です*2。同じくエラーメッセージから探してみます。named-ssdなるマクロでスロットが存在するかを探していますが、最終的には下のようにeqで比較をしています。ということで、確かにパッケージを含む形で比較を行っているようです。

;; L42~@lib/defstruct-lds.lisp
(defmacro defstruct (options &rest slots &environment env)
;; ...(略)...
        (while slots
          (if (atom (car slots))
            (setq name (%car slots) args ())
            (setq name (%caar slots) args (%cdar slots)))
          (unless (symbolp name) (signal-program-error $XNotSym name))
          (unless (setq ssd (named-ssd name slot-list))
            (error "~S has no ~S slot, in ~S"
                   (sd-name sub-sd) name (cons :include include)))
;; ...(略)...
;;L74@lib/defstruct-macros.lisp
(defmacro named-ssd (name slot-list) `(assq ,name ,slot-list))

;; L402~@compiler/optimizers.lisp
(define-compiler-macro assq (item list)
  (let* ((itemx (gensym))
         (listx (gensym))
         (pair (gensym)))
    `(let* ((,itemx ,item)
            (,listx ,list))
      (dolist (,pair ,listx)
        (when (and ,pair (eq (car ,pair) ,itemx)) (return ,pair))))))

SBCLはスロット名なんてパッケージ関係ないよねという考え方…かと思いきや、with-slotsslot-valueではやっぱりパッケージを考慮しているので、なんだか半端です。この辺りも考え合わせると、SBCLのやり方が特殊という感じがします。どういった考えなんでしょう。

*1:単に普段利用している処理系というだけです

*2:古いですが、前に1.10を入れようとしたところ、手元のCentOS 6.5ではglibcか何かが古くて入らなかったため放置してます…