Common Lispの構造体メカニズムは、Cのstruct型に類似する
データ型を定義する一般的な方法を提供する。構造体は、いくつかの数の
スロットを含むLispオブジェクトであり、スロットのそれぞれは任意の
Lispデータオブジェクトを保持できる。スロットのアクセスや設定、構造体
オブジェクトの作成やコピー、ある特定の構造体の型のオブジェクトを
認識する関数が提供される。
真のCommon Lispでは、それぞれの構造体型は、すべての存在するLisp型とは 別の新しい型である。下にあるEmacs Lispシステムは、新しい別の型を作る 方法を提供しないので、このパッケージは、識別するための特殊な“タグ” シンボルを伴うベクタ(要求によってはリスト)として構造体を実装する。
defstructフォームは、nameと呼ばれる、指定された
slotsを持つ新しい構造体型を定義する(slotsは構造体型を
記述する文字列で始まってもよい)。単純な場合、nameやslots
のそれぞれはシンボルである。たとえば、
(defstruct person name age sex)
は、3つのスロットを含む、personと呼ばれる構造体型を定義する。
personオブジェクトpが与えられると、
(person-name p)や(person-age p)、
(person-sex p)を呼ぶことでそれらのスロットに
アクセスできる。これらのすべてのplaceフォームでsetfを
使うことで、これらのスロットを変更することもできる:
(incf (person-age birthday-boy))
make-personを呼び出すことで、新しいpersonを
作ることができる。make-personは、新しいオブジェクトのスロットの
初期値を指定するために、キーワード引数:nameと:age、
:sexを取る(これらの引数のいずれかを省略すると、Common Lisp
標準によれば、対応するスロットは“未定義”のままである; Emacs Lisp
では、このような初期化されていないスロットはnilで埋められる)。
personが与えられると、(copy-person p)は、その
スロットがpのそれとeqである同じ型の新しいオブジェクトを
作る。
任意のLispオブジェクトxが与えられると、(person-p x)
はxがpersonのように見える場合は真を、さもなければ偽を
戻す(再び、Common Lispではこの述語は正確だろう; Emacs Lispでできる
最善のことは、xが正しいタグシンボルで始まる正しい長さの
ベクタであることを確かめることである)。
person-nameのようなアクセサは、通常は(実際にperson-pを
使って)それらの引数をチェックし、引数が不正な型の場合はエラーを
通知する。このチェックは、(optimize (safety …))
宣言によって影響される。既定のSafetyレベル1は、すべての不正な引数を
見つける、いくぶん最適化されたチェックを使うが、無益な
エラーメッセージを使うかもしれない(たとえば、
“expected a person”ではなく“expected a vector”のように)。
Safetyレベル0は、下にあるaref呼び出しによって提供されるものを
除いて、すべてのチェックを省略する; Safetyレベル2と3は、不正な入力に
対して常に記述的なエラーメッセージを印字する、厳しいチェックを行なう。
See 宣言.
(setq dave (make-person :name "Dave" :sex 'male))
⇒ [cl-struct-person "Dave" nil male]
(setq other (copy-person dave))
⇒ [cl-struct-person "Dave" nil male]
(eq dave other)
⇒ nil
(eq (person-name dave) (person-name other))
⇒ t
(person-p dave)
⇒ t
(person-p [1 2 3 4])
⇒ nil
(person-p "Bogus")
⇒ nil
(person-p '[cl-struct-person counterfeit person object])
⇒ t
一般に、nameは名前のシンボルか、名前のシンボルの後に任意の数の 構造体オプションが続くリストである; それぞれのslotは、 スロットシンボルか、フォーム ‘(slot-name default-value slot-options…)’ のリストである。default-valueは、スロットの値を指定せずに 構造体型のインスタンスが作られるすべてのときに評価されるLisp フォームである。
Common Lispは、いくつかのスロットオプションを定義しているが、
このパッケージで実装されている唯一のものは:read-onlyである。
このオプションに対する非nilの値は、そのスロットはsetf
可能であってはならないということを意味する; スロットの値は、
オブジェクトが作られたときに決定され、その後は変わらない。
(defstruct person (name nil :read-only t) age (sex 'unknown))
:read-only以外のすべてのスロットオプションは無視される。
不明瞭な歴史的な理由のため、構造体オプションはスロットオプションと 異なるフォームを取る。構造体オプションは、キーワードシンボルか、 キーワードシンボルで始まり、ことによると引数が続くリストである (対称的に、スロットオプションはリストで囲まれないキーと値の対である)。
(defstruct (person (:constructor create-person)
(:type list)
:named)
name age sex)
下記の構造体オプションが認められる。
:conc-name引数は、その印字名がスロットアクセサ関数の名前のために接頭辞として
使われるシンボルである。既定は、構造体型の名前にハイフンが
続いたものである。オプション(:conc-name p-)は、この接頭辞を
p-に変える。引数としてnilを指定することは接頭辞なしを
意味するので、スロット名自身がアクセサ関数を名付けるために使われる。
:constructor単純な場合、このオプションは、コンストラクタ関数のために使われる代替の
名前である1つの引数を取る。既定は、たとえばmake-personのように
make-nameである。上記の例は、これをcreate-personに
変える。引数としてnilを指定することは、標準の
コンストラクタはまったく生成されないことを意味する。
このオプションの完全なフォームでは、コンストラクタ名には任意の
引数リストが続く。Common Lisp引数リストのフォーマットの記述は、
See プログラム構造. &restや&keyのようなすべての
オプションがサポートされる。引数名はスロット名とマッチすべきである;
それぞれのスロットは、対応する引数で初期化される。その名前が引数
リストに現れないスロットは、スロット記述子のdefault-valueに
基づいて初期化される。また、既定を指定しない&optionalと
&key引数は、スロット記述子からその既定を取る。スロット名に
対応しない引数を含むことは正しい; スロットに対応する付加引数や
キーワード引数、&aux引数のための既定で参照される場合、それらは
有用である。
1つの構造体に、完全なフォーマットの:constructor
オプションをいくつでも指定できる。単純フォーマットの
:constructorオプションで無効にしない限り、既定の
コンストラクタも同様に生成される。
(defstruct
(person
(:constructor nil) ; no default constructor
(:constructor new-person (name sex &optional (age 0)))
(:constructor new-hound (&key (name "Rover")
(dog-years 0)
&aux (age (* 7 dog-years))
(sex 'canine))))
name age sex)
ここでの第1のコンストラクタは、キーワードによってではなく、位置でその
引数を取る(公式なCommon Lispの用語では、キーワードの代わりに引数の順序
によって働くコンストラクタは、“BOAコンストラクタ”と呼ばれる。いや、
私がこれをでっちあげているのではない)。たとえば、
(new-person "Jane" 'female)は、そのスロットがそれぞれ
"Jane"と0、femaleであるpersonを生成する。
第2のコンストラクタは、2つのキーワード引数を取る。:nameは、
nameスロットを初期化し、"Rover"を既定とする。
:dog-yearsは、それ自身があるスロットに対応するのではないが、
ageスロットを初期化するために使われる。sexスロットは、
強制的にシンボルcanineになり、それを無効にする文法を持たない。
:copier引数は、この方のコピー関数の代替名である。既定はcopy-name
である。nilはコピー関数を生成しないことを意味する(この実装では、
すべてのコピー関数は、単にcopy-sequenceの同義語である)。
:predicate引数は、この型のオブジェクトを認識する述語の代替名である。既定は
name-pである。nilは述語関数を生成しないことを意味
する(:typeオプションが、:namedオプションなしで使われる
場合、述語は決して生成されない)。
真のCommon Lispでは、:predicateが使われても、typepは常に
構造体オブジェクトを認識できる。このパッケージでは、typepは単に
typename-pと呼ばれる関数を探すので、既定の述語名を使う
場合だけ構造体型に対して動作するだろう。
:includeこのオプションは、とても制限されたC++スタイルの継承の形式を実装する。
引数は、前もってdefstructで作られた別の構造体型の名前である。
その効果は、新しい構造体型が、含められた構造体型のすべてのスロット
(加えて、もちろんこの構造体のスロット記述子で記述されたすべての新しい
スロットも)を受け継ぐようにすることである。新しい構造体は、含められた
構造体の“特殊化”と考えられる。事実、含められた型の述語や
スロットアクセサは、新しい型のオブジェクトも受け入れる。
含められた構造体名の後に、:includeオプションへ余分の引数がある
場合、これらのオプションは、含められた構造体のスロットのための、
ことによると修正された既定値を伴うスロット記述子の置き換えとして
扱われる。Steeleから例を借りると:
(defstruct person name (age 0) sex)
⇒ person
(defstruct (astronaut (:include person (age 45)))
helmet-size
(favorite-beverage 'tang))
⇒ astronaut
(setq joe (make-person :name "Joe"))
⇒ [cl-struct-person "Joe" 0 nil]
(setq buzz (make-astronaut :name "Buzz"))
⇒ [cl-struct-astronaut "Buzz" 45 nil nil tang]
(list (person-p joe) (person-p buzz))
⇒ (t t)
(list (astronaut-p joe) (astronaut-p buzz))
⇒ (nil t)
(person-name buzz)
⇒ "Buzz"
(astronaut-name joe)
⇒ error: "astronaut-name accessing a non-astronaut"
したがって、astronautがpersonの特殊化である場合、
すべてのastronautはpersonでもある(しかし逆は
真ではない)。すべてのastronautは、personのすべての
スロットを含み、加えてastronautに特有の特別なスロットを持つ。
(person-nameのような) people上で働く作用は、ちょうど他のpeople
のようにastronaut上で働く。
:print-function完全なCommon Lispでは、このオプションは、構造体型のインスタンスを
印字するために呼び出される関数を指定することを許す。Emacs Lisp
システムは、Lispプリンタ中にこのような機能を許すフックを
提供しないため、このパッケージは単に:print-functionを無視する。
:type引数は、シンボルvectorまたはlist
のどちらかでなければならない。これは、新しい構造体型を実装するために、
どちらの基礎となるLispデータ型が使われるべきかを語る。ベクタが既定で
使われるが、(:type list)は、その代わりに構造体オブジェクトが
リストとして格納されることを引き起こす。
構造体オブジェクト用にベクタ表現を使うことは、Emacs Lispではベクタを 作ることはやや遅いとはいえ、すべての構造体スロットが素早く アクセスできるという有利な点を持つ。リストは作りやすいが、後の方の スロットをアクセスするのに比較的長い時間がかかる。
:namedこの引数を取らないオプションは、特有の“タグ”シンボルが構造体
オブジェクトの先頭に格納されることを引き起こす。:typeを使い、
かつ:namedを使わないことは、構造体型が識別機能を持たない単なる
ベクタまたはリストとして格納される結果になる。
:typeを明示的に指定しない場合、既定は名前付きベクタを
使うことである。したがって、:namedは:typeとの結合にのみ
有用である。
(defstruct (person1) name age sex)
(defstruct (person2 (:type list) :named) name age sex)
(defstruct (person3 (:type list)) name age sex)
(setq p1 (make-person1))
⇒ [cl-struct-person1 nil nil nil]
(setq p2 (make-person2))
⇒ (person2 nil nil nil)
(setq p3 (make-person3))
⇒ (nil nil nil)
(person1-p p1)
⇒ t
(person2-p p2)
⇒ t
(person3-p p3)
⇒ error: function person3-p undefined
名前のない構造体はタグを持たないので、defstructは、それを
認識するために有用な述語を作ることができない。
また、person3-nameのようなアクセサは生成されるが、どんな型
チェックもできないだろう。たとえばperson3-name関数は、この
場合は単にcarの同義語である。対称的に、person2-nameは
処理の前に、その引数が実際にperson2オブジェクトであるかを
確かめることができる。
:initial-offset引数は非負の整数でなければならない。それは、構造体の先頭で“空”
のままにしておくスロット数を指定する。構造体が名前付きの場合、タグが
リストまたはベクタの指定された位置に現れる; さもなければ、第1の
スロットがその位置に現れる。より早い位置は、コンストラクタによって
nilで満たされ、それ以外は無視される。型が他の型を
:includeする場合、:initial-offsetは、含まれる型の最後の
スロットと最初の新しいスロットの間のスキップされるスロット数を
指定する。
注記されている場合を除いて、このパッケージのdefstruct機能は
Common Lispのそれと完全に互換である。