#:g1: Common Lispの構造体とリストの相互変換色々

Posted 2022-08-21 05:27:45 GMT

こちらの記事を読んで、そういえば構造体やクラスとリストの相互変換の定石ってなんだろうなと思ったので考えてみました。

defstructでリストの構造化された使い方を定義する

正確には構造体とリストの相互変換ではありませんが、普段良く遭遇する局面として、CSVファイル等を読み込んだ結果がリストになっているような場合、car/cdrでごちゃごちゃやるのが面倒なので、defstructでアクセサを作成するということをします。

'(("name" "job")
  ("name" "job")
  ("name" "job")
  ("name" "job")
  )

#| というデータがあった場合に、defstructでリストのアクセサを定義 |#

(defstruct (person (:type list)) name job)

(mapcar #'person-name '(("name" "job") ("name" "job") ("name" "job") ("name" "job") ))("name" "name" "name" "name")

#| 形式が若干違う場合には、 :namedを使ったりオフセットを指定したり |#

(defstruct (person (:type list) :named) name job)

(mapcar #'person-name '((person "name" "job") (person "name" "job") (person "name" "job") (person "name" "job") ))("name" "name" "name" "name")

(defstruct (person (:type list) (:initial-offset 1)) name job)

具体例が、ANSI規格に色々書いてあるので眺めてみるのもいいでしょう。

処理系依存機能で変換する

元記事の作戦は、構造体のリーダーマクロ部分を二文字飛せばplistの表記になるということを利用しています。
そもそも処理系が#Sリーダーマクロを処理する際に(person :name "name" :job "job")というS式から構造体を生成していることが予想されるので、そういうユーティリティが処理系内部にありそうです。

ということでLispWorksの処理系内部を探ってみると、こんな感じに書けました。

#+lispworks
(defun plist->struct (type plist)
  (apply #'structure:make-structure type plist))

(plist->struct 'person '(:name "foo" :job "bar")) → #S(PERSON :NAME "foo" :JOB "bar")

逆方向は色々難しくなりますが、内部の関数を使いまくるとこのようになりました。

#+lispworks
(defun struct->plist (s)
  (let ((dd (structure:structure-dd s)))
    (mapcan (lambda (slot)
              (list (slot-value slot 'structure::name)
                    (structure:z-svref s (slot-value slot 'structure::index))))
            (structure:dd-slots dd))))

(struct->plist (make-person :name "name" :job "job"))(NAME "name" JOB "job")

まず大抵の処理系では構造体はヘッダを持つベクターになっています。このヘッダ情報にコンストラクタやアクセサの情報が詰っているので、その情報を元に構造体からリストを作成します。

MOPを使う

structure-objectに対してstandard-objectの一連のメソッドを適用できるかどうかというのは規格外ですが、大抵の処理系では、スロット付きオブジェクトとして共通化されているので、slot-valueを始めとしてスロット情報を扱うメソッドは使えます。

(defun struct->plist (s)
  (let ((class (class-of s)))
    (mapcan (lambda (slotd)
              (let ((slot-name (slot-definition-name slotd)))
                (list slot-name (slot-value s slot-name))))
            (class-slots class))))

(defun find-structure-constructor (name) (structure:dd-constructor (clos::class-wrapper (find-class name))))

(defun plist->struct (class-name list) (let ((s (funcall (find-structure-constructor class-name)))) (loop :for (prop val) :on list :by #'cddr :do (setf (slot-value s (intern (string prop) (symbol-package class-name))) val)) s))

(plist->struct 'person '(:name "name" :job "job")) → #S(PERSON :NAME "name" :JOB "job")

struct->plistは標準のMOPに収まりますが、plist->structの方はタイプ名から構造体の情報を引き出す標準的な方法がなかったりスロット名シンボルの扱い等にやや難があります。

ついでにchange-class

変換といえばchange-classなのでついでに書いてみました。
ちなみに、standard-class間以外でのclange-classは未定義動作だった気がします(が記述がぱっとみつけられない)。

スロットアクセスの最適化等が実施されるとプログラマの記述したアクセス方法を使うとは限らないので、change-classを実施するとスロットの値が行方不明になったりする可能性もあります。
その辺りを含めて利用には注意が必要ですが、外部データの読み込みや、データ変換等では便利に使えるので、用法をおさえれば活用できるのではないでしょうか。

(defmethod change-class ((s person) (to-class (eql (find-class 'list))) &rest initargs)
  (struct->plist s))

(defmethod change-class ((list list) (to-class structure-class) &rest initargs) (plist->struct (class-name to-class) list))

(change-class (change-class (make-person :name "foo" :job "bar") (find-class 'list)) (find-class 'person)) → #S(PERSON :NAME "foo" :JOB "bar")

リーダーを経由して変換する場合の問題点

ちなみにリーダーを経由しない手法を長々と書いてきましたが、リーダーを経由する(文字列での)変換には、読み込み特有の問題があります。 リストからの変換では、読み込み不能オブジェクトは扱えず、構造体からの変換では#Sが禁止される(まずないですが)と機能しなくなります。

(let ((*read-suppress* T))
  (struct->list (make-person :name "name" :job "job")))
→ NIL
  NIL

(list->struct 'person (list :name nil :job *readtable*)) >>> READ encountered #<, an unknown format.

まとめ

構造体⇔リストの変換を色々考察してみました。
個人的な感覚としては、既存のリスト構造に対してdefstructでリストのアクセサを定義して問題解決ということが多い気がしています。


HTML generated by 3bmd in LispWorks 8.0.1

comments powered by Disqus