7章Objectsのクラス関連の和訳
【追記】7章Objectsの全てを日本語訳しました。
https://nptcl.github.io/npt-japanese/docs/ansicl/7.html
https://nptcl.github.io/npt-japanese/md/ansicl/7.html
ふたつのリンクは、レイアウトが違うだけでどちらも同じ内容なので好きな方を見てください。
リンク先はDictionaryも翻訳してます。
ただし翻訳してあるのは7章だけです。
ANSI Common Lispの規格書の、「7. Objects」の一部の和訳です。
必要になったので必死に翻訳しました。
訳があっているかどうかは知らん。
翻訳元は下記の通り。
draft proposed American National Standard for Information Systems Programming Language Common Lisp Version 15.17R, X3J13/94-101R. Fri 12-Aug-1994 6:35pm EDT http://www.cs.cmu.edu/afs/cs/Web/Groups/AI/lang/lisp/doc/standard/ansi/dpans/
HyperSpecだと下記の通り。
7. Objects http://www.lispworks.com/documentation/HyperSpec/Body/07_.htm
7.6 Generic Functions and Methods
以降は長いのでまた今度。
【追記】続きを投稿しました。7章Objectsのジェネリック関数関連の和訳
目次
7. オブジェクト 7.1 オブジェクトの作成と初期化 7.1.1 初期化引数 7.1.2 初期化引数の有効性の宣言 7.1.3 初期化引数のデフォルト値 7.1.4 初期化引数の規則 7.1.5 Shared-Initialize 7.1.6 Initialize-Instance 7.1.7 Make-InstanceとInitialize-Instanceの宣言 7.2 インスタンスのクラスの変更 7.2.1 インスタンスの構造の修正 7.2.2 新しく追加された局所スロットの初期化 7.2.3 インスタンスのクラスの更新のカスタマイズ 7.3 インスタンスの再初期化 7.3.1 再初期化のカスタマイズ 7.4 メタオブジェクト 7.4.1 標準メタオブジェクト 7.5 スロット 7.5.1 スロットの紹介 7.5.2 スロットへのアクセス 7.5.3 スロットの継承とスロットオプション 【別投稿】7.6 ジェネリック関数とメソッド
7. オブジェクト
7.1 オブジェクトの作成と初期化
ジェネリック関数make-instance
は、クラスの新しいインスタンスを作成し返却します。
最初の引数はクラスか、クラスの名前であり、残りの引数は初期化引数リストです。
新しいインスタンスの初期化は、いくつかのステップから成ります。
内容は次のようになります。
指定されなかった初期化引数の値に対して、明に指定された初期化引数とデフォルト値を結びつけるステップ。
初期化引数の有効性をチェックするステップ。
インスタンスの記憶領域を確保するステップ。
スロットに値を埋めるステップ。
そして追加の初期化を行うためにユーザーが提供したメソッドを実行するステップ。
make-instance
の各ステップはジェネリック関数により実装されているため、
それぞれのステップをカスタマイズする仕組みが提供されています。
加えて、make-instance
自体がジェネリック関数であるため、自身もカスタマイズできます。
オブジェクトシステムは各ステップに対して、システムで提供されたメソッドを用意しています。 メソッドは初期化全体の手順の標準的な振る舞いを定義したものです。 標準の振る舞いは、下記の4つの簡単な仕組みによって、初期化を制御することができます。
スロットの初期化引数としてのシンボルの宣言。 初期化引数は
defclass
のスロットオプションである、:initarg
を使うことで宣言できます。 これは、make-instance
の呼び出し時に、 スロットの値を設定するための仕組みとして提供されたものです。初期化引数のデフォルト値フォームの指定。初期化引数のデフォルト値フォームは、
defclass
のクラスオプションである:default-initargs
を使うことで定義できます。 もし初期化引数がmake-instance
の引数として明に提供されなかった場合、 デフォルト値フォームはdefclass
が宣言されたレキシカル環境の中で評価されます。 そして評価された結果の値は、初期化引数の値として使用されます。スロットのデフォルト初期化値フォームの提供。スロットのデフォルト初期化値フォームは、
defclass
のスロットオプション:initform
を使うことで宣言されます。 もしmake-instance
の引数かあるいは:default-initargs
のデフォルト値にて、 スロットに対応する初期化引数が与えられていなかった場合、 デフォルト値フォームはdefclass
が宣言されたレキシカル環境の中で評価されます。 そして評価された結果の値はスロットに格納されます。 局所スロットの:initform
フォームは、インスタンスが作成されたとき、 クラスの再定義によりインスタンスを更新するとき、 そしてインスタンスを違うクラスの定義に更新するときに使用されるでしょう。 共有スロットの:initform
フォームに関しては、 定義のときか、再定義のときに使用されます。initialize-instance
とshared-initialize
のメソッド定義。 スロットの値を設定するこれらの振る舞いは、 システムが提供するメソッドで提供されており、initialize-instance
は、shared-initilize
を呼び出すように実装されています。 ジェネリック関数shared-initialize
は初期化の部分を実装しており、 次の4つの状況で共有されています。 それは、インスタンス作成時、インスタンスの再初期化時、 クラスの再定義によるインスタンスの更新時、 そして違うクラス定義へインスタンスを更新するときです。 システムが提供するshared-initialize
のメソッドは、 スロットの値を更新するための上記の振る舞いを直接実装しているため、initialize-instance
は単純にshared-initialize
を呼び出すだけとなります。
7.1.1 初期化引数
初期化引数は、オブジェクトの作成と初期化を制御します。
よくキーワードを初期化引数の名前にするのが便利で使われますが、
初期化引数の名前はnil
を含むどんなシンボルでも使用できます。
初期化引数は、次の2つの方法である、
スロットの値を埋めるためか、
あるいは初期化メソッドの引数に提供するときに使用します。
単一の初期化引数は、両方の目的で使用されます。
初期化引数リストは、初期化引数の名前と値のプロパティリストです。
この構造は、通常のプロパティリストとして同一であり、
引数リストの&key
パラメーターとして処理される部分としても同一です。
これらのリストは、もし初期化引数の名前が初期化引数リストに複数現れた場合は、
もっとも左側に現れた値が指定され、残りのものは無視されます。
make-instance
の引数(最初の引数よりあとのもの)の形は、初期化引数リストです。
初期化引数はスロットと結び付けることができます。
もし初期化引数が初期化引数リストの中で値を持っている場合、
その値は新しく作成されたオブジェクトのスロットに格納されます。
もし:initform
フォームがスロットと結び付けられていた場合でも、初期化引数の方が上書きをします。
1つの初期化引数は、複数のスロットを初期化することができます。
共有スロットを初期化する初期化引数は、以前の値を置き換えて、共有スロットに値を格納します。
初期化引数はメソッドに結び付けることができます。
オブジェクトが作成されて、特定の初期化引数が与えられた場合、
ジェネリック関数であるinitialize-instance
, shared-initialize
, そしてallocate-instance
は、
キーワード引数のペアとして、初期化引数の名前と値とともに呼び出されます。
もし初期化引数の値が初期化引数リストで提供されていなかった場合は、
メソッドのラムダリストがデフォルト値を提供します。
初期化引数は次の4つの状況によって使用されます。 インスタンスの作成時、インスタンスの再初期化時、 クラス再定義によるインスタンスの更新時、そして違うクラス定義へのインスタンスを更新するときです。
初期化引数は特定のクラスのインスタンスの作成と初期化時に制御で使用されるため、 初期化引数は、クラスの「初期化引数は〜」のように記述します。
7.1.2 初期化引数の有効性の宣言
初期化引数は、4つの状況にて有効性がチェックされます。
初期化引数はひとつの状況では有効かもしれませんが、他はそうではないかもしれません。
例えば、システムで提供されたmake-instance
のメソッドのstandard-class
クラスを対象とした場合を考えます。
もし初期化引数が与えられていたものの、有効性としての宣言がされていなかった場合、
メソッドは初期化引数の有効性チェックにおいてエラーのシグナルを発することになります。
初期化引数の有効性の宣言は、次の2つの意味があります。
スロットを設定するときの初期化引数は、
defclass
のスロットオプションである:initarg
によって有効であるとして宣言されます。 スロットオプションの:initarg
は、スーパークラスから継承されます。 よって、クラスのスロット設定時の有効な初期化引数の集合は、 クラスとスーパークラスによって有効であると宣言されたスロット設定時の初期化引数の和集合となります。 初期化引数による値の設定は、4つの状況すべてにおいて有効です。メソッドの引数として与えられた初期化引数は、 これらメソッドの宣言によって有効であると定義されます。 メソッドのラムダリストとして定義されたキーワードパラメーターのキーワード名は、 全てのクラスの適用可能なメソッドの初期化引数となります。 適用されるメソッドのラムダリストにある
&allow-other-keys
の存在は、 初期化引数の有効性のチェックを無効にします。 よってメソッドの継承は、メソッドに引数として渡される有効な初期化引数の集合を制御します。 メソッドの定義を持ったジェネリック関数は、下記に示すものとして、 有効な初期化引数の宣言を守ります。関数
allocation-instance
,initialize-instance
とshared-initialize
により、 クラスのインスタンスを作成するとき。 クラスのインスタンスを作成するとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。関数
reinitialize-instance
とshared-initialize
により、インスタンスの再初期化が行われるとき。 インスタンスの再初期化が行われるとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。関数
update-instance-for-redefined-class
とshared-initialize
により、 再定義されたクラスにインスタンスを更新するとき。 再定義されたクラスにインスタンスを更新するとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。関数
update-instance-for-different-class
とshared-initialize
により、 違うクラスの定義にインスタンスを更新するとき。 違うクラスの定義にインスタンスを更新するとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。
クラスの有効な初期化引数の集合は、スロットの値の設定か、
あるいは初期化引数の前宣言として与えられる:allow-other-keys
に従った
メソッドの引数かのどちらかの初期化引数の集合です。
:allow-other-keys
のデフォルト値はnil
です。
もし初期化引数:allow-other-keys
の値がtrue
であるならば、
初期化引数の有効性の確認は無効となります。
7.1.3 初期化引数のデフォルト値
クラスオプションである:default-initargs
を使うことで、
初期化引数のデフォルト値フォームを提供することができます。
もしいくつかのクラスによって初期化引数が有効であると宣言された場合は、
デフォルト値フォームは違うクラスによって設定されるかもしれません。
このような場合では、:default-initargs
は継承された初期化引数によって提供されたデフォルト値が使用されます。
オプション:default-initargs
は、初期化引数へのデフォルト値の提供のみに使用されます。
このオプションでは、シンボルを有効な初期化引数の名前として宣言しません。
さらに、オプション:default-initargs
は、
インスタンス作成時における初期化引数のデフォルト値の提供としてのみ使用されます。
クラスオプションの引数である:default-initargs
は、
初期化引数の名前とフォームが交互に現れるリストです。
各フォームは、初期化引数に対応するデフォルト値のフォームです。
初期化引数のデフォルト値のフォームは、
make-instance
の引数に初期化引数として現れていなかった場合、
かつ、もっと特定的なクラスによってデフォルト値が定義されていなかった場合のみに、
評価されて使用されます。
デフォルト値のフォームは、defclass
フォームのレキシカル環境で評価されたものが提供され、
評価された結果は初期化引数の値として使用されます。
make-instance
に指定された初期化引数は、
デフォルトの初期化引数と結び付けられ、
デフォルト初期化引数リストを生成します。
デフォルト初期化引数は、初期化引数の名前と値を交互にリストにしたものです。
このリストは、指定されていない初期化引数のデフォルト値を決定するものであり、
また明示的に初期化引数が指定されたものは、
デフォルト初期化引数リストのより早く表れたもののリストとします。
デフォルト初期化引数は、クラス優先リストの順番に従ったクラスのデフォルト値に順番付されます。
:default-initargs
と:initform
では、どちらもスロットの初期化に使用されますが、
両者の間には目的に違いがあります。
クラスオプションである:default-initargs
は、ユーザーに対して、
初期化引数がスロットを初期化されているかどうか、
あるいはメソッドに渡されるかどうかを知ることなしに、デフォルト値を与える仕組みを提供します。
もしmake-instance
を呼ぶ際に、初期化引数を明示的に与えなかった場合はデフォルト値が使用されますが、
デフォルト値は呼び出し時に指定されたものとして呼び出されます。
対称的に、スロットオプションである:initform
は、
ユーザーがスロットのデフォルト値フォームを与えるための仕組みとして提供されます。
:initform
フォームはスロットの初期化に使用されますが、
ただmake-instance
に与えられた初期化引数に対応するスロットとの結びつきがなかった場合、
あるいは:default-initargs
にデフォルト値の指定がなかった場合のみ、:initform
にて初期化が行われます。
初期化引数のデフォルト値フォームの評価順序と、
:initform
フォームの評価順序は定義されてはいません。
もし評価順序が重要である場合は、
代わりにinitialize-instance
かshared-initialize
メソッドを使用してください。
7.1.4 初期化引数の規則
スロットオプションの:initarg
は、スロット対して複数定義されるかもしれません。
もし初期化引数に複数の定義がされるかもしれないときには、下記に示すルールが適応されます。
もし同じ初期化引数の名前が
:initarg
スロットオプションに複数現れた場合は、 初期化引数は複数のスロットを初期化できます。初期化引数の名前は、複数の初期化メソッドのラムダリストに現れます。
初期化引数の名前は、スロットオプションの
:initarg
と、初期化メソッドのラムダリストの両方に現れます。
もしmake-instance
に与えられた引数が、同じスロットを初期化するような複数の初期化引数であった場合、
さらに初期化引数が違った名前であったときは、初期化引数リストの最も左の初期化引数の値が採用されます。
もし複数の違った初期化引数が同じスロットを初期化する場合、
さらにスロットはデフォルト値を持っており、
make-instance
の引数には明示的に指定されていなかったときは、
初期化引数は最も特定的なクラスのクラスオプション:default-initargs
に現れる値が採用されます。
もしひとつの:default-initargs
クラスオプションが、
複数の初期化引数により同じスロットを初期化する場合、
さらにmake-instance
の引数には明示的に指定がなかったときは、
クラスオプション:default-initargs
の最も左側の値が採用され、
残りのデフォルト値フォームの値は無視されます。
make-instance
の引数として明示的に与えられた初期化引数は、
デフォルト初期化引数の左側に現れます。
例えば、クラスC1
とC2
が違うスロットに対してデフォルト初期化引数の値を与えた場合を考えます。
C1
はC2
よりも特定的であるとします。
C1
によって提供されたデフォルト初期化引数は、
デフォルト初期化引数リストにおいては、
C2
によって提供されたのデフォルト初期化引数の左側に位置します。
もし単一のクラスオプション:default-initargs
が、
2つの違ったスロットに対して初期化引数の値が与えられた場合、
クラスオプション:default-initargs
の最も左に位置する初期化引数が、
デフォルト初期化引数リストの最も左側に現れます。
もしスロットが:initform
フォームと:initarg
スロットオプションの両方を持っており、
さらに初期化引数が:default-initargs
によるデフォルト値により与えられているか、
あるいはmake-instance
の引数により与えられていた場合、
:initform
フォームは使われませんし評価もされません。
上記の規則の例を示します。
(defclass q () ((x :initarg a))) (defclass r (q) ((x :initarg b)) (:default-initargs a 1 b 2))
フォーム デフォルト初期化引数リスト スロットXの値 ---------- (make-instance 'r) (a 1 b 2) 1 (make-instance 'r 'a 3) (a 3 b 2) 3 (make-instance 'r 'b 4) (b 4 a 1) 4 (make-instance 'r 'a 1 'a 2) (a 1 a 2 b 2) 1
7.1.5 Shared-Initialize
ジェネリック関数shared-initialize
は、
インスタンスの作成時、インスタンスの再初期化時、
クラス再定義によるインスタンス更新時、違うクラスへのインスタンス更新時において、
インスタンスのスロット値を、初期化引数か:initform
フォームによって設定する際に使用されます。
method-combinationはstandard
が使用されます。
引数は次のような順番で受け取ります。
初期化されるインスタンス、インスタンスのアクセス可能なスロット名の集合、
そして任意の長さの初期化引数です。
最初2つよりあとの引数は、初期化引数リストの形にしなければなりません。
shared-initialize
の2番目の引数は、下記のどちらかに従います。
引数はスロットの名前のリスト(空リストでも可)であり、 スロット名の集合を指定したものです。
引数はシンボル
t
であり、すべてのスロットの集合を指定したものです。
システムが提供しているshared-initialize
のメソッドでは、
第一引数の特定パラメーターがstandard-object
クラスのものが存在します。
このメソッドは共有か局所かに関わらず、各スロットに対して次の振る舞いを行います。
もし初期化引数リスト中の初期化引数がスロットへの値を特定した場合は、 メソッドが実行する前に対象のスロットにすでに値が格納されていても、 スロットへ特定した値が格納されます。 影響があるスロットは、
shared-initialize
の第二引数で指定されたスロットとは独立しています。第二引数によって指定されたどんなスロットも、 この時点においてまだ
unbound
であった場合は、:initform
フォームに従って初期化されます。:initform
フォームを持つどのスロットも、 フォームはdefclass
宣言のレキシカル環境にて評価され、 その結果がスロットに格納されます。 例えば、before
メソッドがスロットへ値を格納する場合、:initform
フォームはスロットへの値の格納には使用されないでしょう。 もし第二引数が指定した名前が、 インスタンスのアクセス可能なスロットに対応していなかった場合は、 結果は定義されていません。この規則は7.1.4 初期化引数に従います。
ジェネリック関数shared-initialize
は、
システムが提供するメソッドreinitialize-instance
,
update-instance-for-different-class
, update-instance-for-redefined-class
,
そしてinitialize-instance
によって呼び出されます。
このようにメソッドは
これらすべてのコンテキスト上で実行できるようなアクションを指定するように、
shared-initialize
を記述することができます。
7.1.6 Initialize-Instance
ジェネリック関数initialize-instance
は、
新しく作成されたインスタンスを初期化するために、
make-instance
によって呼び出されます。
method-combinationはstandard
が使われます。
initialize-instance
のメソッドは、
単純に初期値をスロットに指定できないような初期化を実行するために定義できます。
初期化中では、次に示したアクションを実行したあとにinitialize-instance
が呼び出されます。
デフォルト初期化引数リストは、 提供された初期化引数リストと各クラスのデフォルト初期化引数を結びつける計算がされます。
デフォルト初期化引数リストの有効性はチェックされます。 もしどの初期化引数の有効として宣言されていなかった場合は、エラーが発せられます。
新しいインスタンスはスロットが
unbound
として作成されます。
ジェネリック関数initialize-instance
は
新しいインスタンスとデフォルト初期化引数とともに呼び出されます。
システムが提供するinitialize-instance
のメソッドでは、
特定パラメーターはstandard-object
クラスのものが存在します。
このメソッドは、ジェネリック関数shared-initialize
を呼び出し、
初期化引数に対応したものか、
あるいは:initform
フォームに対応した値を設定します。
ジェネリック関数shared-initialize
の引数は、
インスタンス、t
、デフォルト初期化引数を指定して呼び出されます。
注意として、initialize-instance
はデフォルト初期化引数リストを
shared-initialize
の呼び出し時に提供します。
そして最初のステップとして、
システムが提供するshared-initialize
のメソッドは、
make-instance
呼び出し時に提供された初期化引数と、
デフォルト初期化引数リストの両方を集計して呼び出されます。
initialize-instance
のメソッドは、
インスタンスの初期化時に、特定のアクションを定義することができます。
もしinitialize-instance
のafter
メソッドだけが定義された場合、
これらはシステムが提供した初期化後に実行されます。
したがってこれらは、initialize-instance
の標準的な動作には干渉しないでしょう。
オブジェクトシステムは、initialize-instance
メソッドの構築に便利な2つの関数を提供しています。
関数slot-boundp
は、スロットが値を持っているかどうかを示すbool
値を返却します。
これはinstance-initialize
のafter
メソッドを記述する際に、
まだ初期化されていないスロットのみを初期化するような仕組みを提供します。
関数slot-makunbound
は、スロットの値を削除します。
7.1.7 Make-InstanceとInitialize-Instanceの宣言
ジェネリック関数make-instance
は、最適化を考えない場合は、
下記に示す宣言のように実行されます。
(defmethod make-instance ((class standard-class) &rest initargs) ... (let ((instance (apply #'allocate-instance class initargs))) (apply #'initialize-instance instance initargs) instance)) (defmethod make-instance ((class-name symbol) &rest initargs) (apply #'make-instance (find-class class-name) initargs))
make-instance
の定義で省かれているコードは、
initargs
をデフォルト初期化引数によって指定する部分であり、
また初期化引数の結果を初期化引数に設定するかどうか決定するために、
スロットに値が設定されておらず、
メソッドの引数として供給もされていないかどうかをチェックする部分となります。
ジェネリック関数initialize-instance
は、最適化を考えない場合は、
下記に示す宣言のように実行されます。
(defmethod initialize-instance ((instance standard-object) &rest initargs) (apply #'shared-initialize instance t initargs)))
これらのコードはカスタマイズ可能です。
プログラマーへのインターフェイスレベルとしてカスタマイズできるものは、
defclass
のオプションである、:initform
, :initarg
そして:default-initargs
が含まれますし、
同様にmake-instance
, allocate-instance
, そしてinitialize-instance
のメソッド宣言があげられます。
shared-initialize
のメソッドを定義することも可能です。
この関数は、ジェネリック関数のreinitialize-instance
, update-instance-for-redefined-class
,
update-instance-for-defferent-class
, そしてinitialize-instance
によって実行されます。
メタオブジェクトレベルでは、追加でカスタマイズをサポートします。
処理系は、initialize-instance
とshared-initialize
について明確な最適化を許容しています。
7章にあるshared-initialize
の定義では、可能な最適化についての説明があります。
7.2 インスタンスのクラスの変更
関数change-class
は、
インスタンスのクラスを現在のクラスCfrom
から違うクラスCto
へ変更する際に使用します。
この関数はインスタンスの構造を変化させて、
クラスCto
の定義へ適用させるような変更を行います。
インスタンスのクラスの変更は、
スロットへの追加と削除が行われるでしょう。
インスタンスのクラスの変更は、関数eq
により定義されたものとの同一性を変更しません。
関数change-class
がインスタンスに対して実行されたとき、
2つの手順により更新が行われます。
最初の手順は、新しい局所スロットの追加と、
新しいバージョンには定義されない局所スロットの削除によるインスタンスの構造の変更です。
二番目の手順は、新しく追加された局所スロットの初期化と、
他にユーザーが定義したアクションの実行です。
これら2つの手順は、次に示す2つの章によって定義されています。
7.2.1 インスタンスの構造の修正
インスタンスをクラスCto
へ修正するために、
クラスCto
には定義されているがCfrom
には定義されていない局所スロットは追加され、
クラスCto
には定義されていないがCfrom
には定義されている局所スロットは削除されます。
クラスCto
とクラスCfrom
の両方に定義されている局所スロットの値は保持されます。
もし局所スロットがunbound
であった場合は、unbound
のままです。
クラスCfrom
では共有スロットであり、Cto
では局所スロットとして定義された
スロットの値は保持されます。
最初の更新ステップでは、どの共有スロットの値も影響がありません。
7.2.2 新しく追加された局所スロットの初期化
更新の二番目の手順では、新しく追加されたスロットを初期化し、
ユーザー定義のアクションを実行します。
このステップは、ジェネリック関数update-instance-for-different-class
によって定義されます。
ジェネリック関数update-instance-for-different-class
は、
最初の更新手順が完了したあとに、change-class
によって実行されます。
ジェネリック関数update-instance-for-different-class
は、
change-class
によって計算された引数により実行されます。
最初の引数は、更新されるインスタンスのコピーであり、
クラスCfromの
インスタンスです。
このコピーは、ジェネリック関数change-class
に動的エクステントとして保有されます。
二番目の引数は、change-class
によって更新されるインスタンスであり、
クラスCto
のインスタンスです。残りの引数は、初期化引数リストです。
システムが提供するupdate-instance-for-different-class
メソッドは、
2つの特定パラメーターがあり、
どちらもstandard-object
クラスです。
最初、このメソッドは、初期化引数の有効性をチェックし、
もし指定された初期化引数が有効であると宣言されていなかった場合は、
エラーが発せられます(詳細は7.1.2 初期化引数の有効性の宣言を参照)。
それから、このメソッドはジェネリック関数shared-initialize
を、
次に示す引数とともに呼び出します。
引数は、新しいインスタンス、新しく追加されるスロット名のリスト、
そして受け取った初期化引数です。
7.2.3 インスタンスのクラスの更新のカスタマイズ
update-instance-for-different-class
のメソッドは、
インスタンスを更新するとき、
特定のアクションを定義することができます。
もしupdate-instance-for-different-class
にafter
メソッドのみが定義された場合は、
これはシステムが提供する初期化のメソッドのあとに実行されます。
よってupdate-instance-for-different-class
の標準的な動作には干渉しないでしょう。
shared-initialize
のメソッドは、クラスの再定義を
カスタマイズするために定義されるでしょう。
詳細は7.1.5 Shared-Initializeを参照。
7.3 インスタンスの再初期化
ジェネリック関数reinitialize-instance
は、
初期化引数に従ってスロットの値を変更するときに使用されます。
再初期化のプロセスにより、スロットの値が変更され、
ユーザーが定義するアクションが実行されます。
これはスロットの追加と削除といったインスタンスの構造の修正は行いません。
また、:initform
フォームを使ったスロットの初期化を行いません。
ジェネリック関数reinitialize-instance
は、直接呼び出されるでしょう。
これは引数に一つのインスタンスが要求されます。
またreinitialize-instance
かshared-initialize
によって使用される、
任意の数の初期化引数を受け取ります。
要求されるインスタンスの引数より後の引数は、初期化引数リストの形式でなければなりません。
システムが提供するreinitialize-instance
のメソッドは、
特定パラメーターにstandard-object
クラスを取ります。
最初、メソッドは初期化引数の有効性をチェックし、
もし指定された初期化引数が有効であると宣言されていなかった場合は、
エラーが発せられます(詳細は7.1.2 初期化引数の有効性の宣言を参照)。
このメソッドは、ジェネリック関数shared-initialize
を、
次に示す引数とともに呼び出します。
引数は、インスタンス、nil
、そして受け取った初期化引数です。
7.3.1 再初期化のカスタマイズ
メソッドreinitialize-instance
は、インスタンスを更新するとき、
特定のアクションを定義することができます。
もしreinitialize-instance
にafter
メソッドのみが定義された場合は、
メソッドはシステムが提供する初期化のメソッドのあとに実行されます。
よってreinitialize-instance
の標準的な動作に干渉しないでしょう。
shared-initialize
のメソッドは、クラスの再定義を
カスタマイズするために定義されるでしょう。
詳細は7.1.5 Shared-Initializeを参照。
7.4 メタオブジェクト
オブジェクトシステムの実装は、クラス、メソッド、そしてジェネリック関数を扱います。 オブジェクトシステムは、メソッドとクラスによって定義された、 ジェネリック関数の集合を含みます。これらのジェネリック関数の振る舞いは、 オブジェクトシステムの振る舞いを定義します。 これらのメソッドが定義されているクラスのインスタンスは、メタオブジェクトと呼ばれます。
7.4.1 標準メタオブジェクト
オブジェクトシステムは、標準メタオブジェクトと呼ばれる
メタオブジェクトの集合を提供します。
これらはstandard-object
クラスと、
standard-method
, standard-generic-function
, method-combination
の
それぞれのクラスのインスタンスを含みます。
standard-method
クラスは、defmethod
とdefgeneric
フォームによって 定義されるメソッドの標準クラスです。standard-generic-function
クラスは、defmethod
,defgeneric
,defclass
のフォームによって 定義されるジェネリック関数の標準クラスです。standard-object
という名前のクラスは、standard-class
クラスのインスタンスです。 またstandard-object
は、自分自身とstructure-class
を除く、standard-class
のインスタンスである全てのクラスのスーパークラスです。
すべてのmethod-combinationオブジェクトは、
method-combination
クラスのサブクラスのインスタンスです。
7.5 スロット
7.5.1 スロットの紹介
standard-class
がメタクラスのオブジェクトは、
0個かそれ以上の名前の付いたスロットを持ちます。
オブジェクトのスロットは、オブジェクトのクラスによって決められます。
各スロットは、値を保有できます。
スロットの名前は、変数名として使うのに有効な構文のシンボルです。
スロットが値を持っていないときは、そのスロットはunbound
であると言われます。
もしunbound
のスロットを読み込んだ場合は、
ジェネリック関数のslot-unbound
が呼び出されます。
システムが提供するslot-unbound
のメソッドでは、
引数の特定パラメーターがt
クラスのものが提供されており、エラーが発せられます。
もしslot-unbound
が値を返却する場合は、
第一返却値はスロットの値として、そのときに使用されるものとなります。
スロットのデフォルト値フォームは、
スロットオプション:initform
によって定義されます。
:initform
フォームに値が提供された場合は、
defclass
が評価された中のレキシカル環境にてフォームが評価されます。
defclass
が評価された中のレキシカル環境に沿った:initiform
のことを、
補足された初期化フォームと呼びます。
詳細は7.1 オブジェクトの作成と初期化を参照。
局所スロットとして定義されたスロットは、 正確に一つのインスタンスがアクセス可能です。 すなわち、唯一つのスロットが確保されます。 共有スロットとして定義されたスロットは、 クラスとそのサブクラスによって与えられる、複数のインスタンスから見ることができます。
defclass
フォームによるクラスが、スロット特定子に名前を含んでいたとき、
クラスは名前が与えられたスロットが定義されたと言います。
局所スロットの宣言では、即座にはスロットが作成されません。
なぜならクラスのインスタンスが作成されるときに、スロットが作成されるからです。
共有スロットは宣言では、即座にスロットを作成します。
defclass
のスロットオプション:allocation
は、スロットの定義時に種類を指定します。
もしスロットオプション:allocation
の値が:instance
ならば、局所スロットが作成されます。
もしスロットオプション:allocation
が:class
ならば、共有スロットが作成されます。
もしスロットがインスタンスのクラスによって定義された場合、 あるいはクラスのスーパークラスから継承された場合は、 スロットはクラスのインスタンスからアクセス可能であると言います。 インスタンスからは、せいぜい一つの名前付きスロットがアクセス可能です。 クラスによって定義された共有スロットは、 クラスのすべてのインスタンスからアクセス可能です。 スロットの継承による詳細な説明は、7.5.3 スロットの継承とスロットオプションを参照。
7.5.2 スロットへのアクセス
スロットは次の2つの方法にてアクセスできます。
関数slot-value
を使用する方法、
そしてdefclass
フォームによって生成されるジェネリック関数を使用する方法です。
関数slot-value
は、
defclass
フォームで対象のクラスのインスタンスにて
アクセス可能に設定したスロットに対しては、
どんなスロットの名前でも指定してアクセスすることができます。
マクロdefclass
は、スロットの読み書きをするメソッドを生成するための構文が提供されています。
もしreader
のメソッドが要求された場合、
スロットの値を読むためのメソッドが自動的に生成されますが、
ただし値を格納するためのメソッドは生成されません。
もしwriter
のメソッドが要求された場合、
スロットの値を書き込むためのメソッドが自動的に生成されますが、
ただし値を読み込むためのメソッドは生成されません。
もしaccessor
のメソッドが要求された場合は、
スロットの値を読むためのメソッドと、
スロットの値を書き込むためのメソッドが自動的に生成されます。
reader
とwriter
のメソッドは、slot-value
を使用して実装されます。
スロットに対してreader
かwriter
のメソッドを指定するときは、
ジェネリック関数の名前とそれに沿って生成されるメソッドの名前を直接指定します。
もしwriter
メソッドの名前をシンボルname
に指定した場合、
スロットへ書き込むためのジェネリック関数の名前がシンボルname
となり、
そのジェネリック関数の引数は、
新しい値、インスタンスの順に2つ取ります。
もしaccessor
メソッドの名前をシンボルname
に指定した場合、
スロットから読み込むためのジェネリック関数の名前がシンボルname
となり、
そしてスロットへ書き込むためのジェネリック関数の名前が、リストの(setf name)
となります。
スロットオプションの:reader
, :writer
, :accessor
の指定により、
作成か修正が行われたジェネリック関数は、
正確に普通のジェネリック関数として扱うことができます。
注意として、slot-value
はスロットから値を読み込むか書き込む時に使われますが、
そのスロットのreader
かwriter
のメソッドが存在するかどうかに関わらず使用できます。
slot-value
が使われる時、reader
かwriter
のメソッドは実行されません。
マクロwith-slots
は、
指定されたスロットがレキシカル変数として使えるようにするレキシカル環境を確立します。
マクロwith-slots
は、指定したスロットへアクセスするために関数slot-value
を実行します。
マクロwith-accessors
は、
指定したスロットがレキシカル変数としてスロットのaccessor
を通して
使えるようにするレキシカル環境を確立します。
マクロwith-accessors
は、指定したスロットへアクセスするために、
適切なaccessor
を実行します。
7.5.3 スロットの継承とスロットオプション
クラスC
のインスタンスについて、アクセス可能な全てのスロットの名前の集合は、
クラスC
とそのスーパークラスによって定義されたスロットの名前の集合の和集合となります。
インスタンスの構造は、そのインスタンスの局所スロットの名前の集合です。
単純な場合として、たった1つのクラスC
と、そのスーパークラスにて、
名前ありのスロットを定義したとします。
もしスロットがC
のスーパークラスによって定義された場合、
そのスロットは継承されたと言えます。
スロットの特性は、クラス定義のスロット指定子によって決定されます。
スロットS
を定義したクラスを考えます。
もしスロットオプション:alocation
が:instance
ならば、S
は局所スロットであり、
C
の各インスタンスはS
と名前の付いた独自のスロットをもち、
S
には独自の値が格納されます。
もしスロットオプション:allocation
が:class
ならば、
S
は共有スロットであり、S
が定義されたクラスに値が格納されます。
そしてC
の全てのインスタンスは、その1つのスロットにアクセスできます。
もしスロットオプション:allocation
が省略された場合は、:instance
が使用されます。
一般的に、複数のクラスである、C
とそのスーパークラスは、
1つの名前付きのスロットを定義できます。
そのような場合、C
のインスタンスに対しては、
与えられた名前に対してただ1つのスロットがアクセス可能です。
そしてそのスロットの特性は、いくつかのスロットの指定子を
計算によって結び付けたものになります。
計算方法を次に示します。
1つのスロットの名前に対する全てのスロット指定子は、 クラス
C
のクラス優先順位リストのクラスに従って、 最も特定的なものからそうでないものへ順序付けられます。 どのスロット指定子の直下に特定されるかの全ての参照は、この順序に従って調査されます。スロットの確保は、最も特定的なスロット指定子によって制御されます。 もし最も特定的なスロット指定子がスロットオプション
:allocation
を含んでいなかった場合は、:instance
が使用されます。 特定的ではないスロット指定子は、確保には影響しません。スロットのデフォルト初期値フォームは、 スロットオプション
:initform
が含まれる最も特定的なスロット指定子の、:initform
の値になります。 もしスロット指定子が:initform
を持っていなかった場合、 スロットはデフォルト初期値フォームを持ちません。スロットの値は、常に型
(and T1 ... Tn)
となるでしょう。 ここでT1 ... Tn
とは、全てのスロット指定子が持っているスロットオプション:type
の値です。 もしスロット指定子がスロットオプション:type
を持っていなかった場合は、 スロットの値は常に型t
となります。 スロットの型に合っていない値をスロットに格納しようとした結果については未定義です。指定したスロットを初期化する際に使用する初期化引数の集合は、 全てのスロット指定子の中のスロットオプション
:initarg
で定義された初期化引数の和集合です。スロットのドキュメント文字列は、 スロットオプション
:documentation
が含まれる最も特定的なスロット指定子の、:documentation
の値になります。 もしスロット指定子が:documentation
を持っていなかった場合、 スロットはドキュメント文字列を持ちません。
スロットの確保の規則では、共有スロットはシャドウすることができます。
例えば、もしクラスC1
が、スロットの名前S
、
スロットオプション:allocation
が:class
のスロットを定義した場合、
そのスロットは、C1
とその全てのサブクラスのインスタンスからアクセス可能です。
しかし、もしC2
がC1
のサブクラスであり、C2
が名前S
のスロットを定義した場合、
C2
とその全てのサブクラスのインスタンスでは、C1
のスロットは共有されません。
クラスC1
が共有スロットを定義したときは、
次の条件の時にC1
のどんなサブクラスC2
でもその単一のスロットは共有されます。
それは、C2
のdefclass
フォームで同じ名前のスロットを定義していないとき。
あるいは、C2
のクラス優先リスト内において
同じ名前のスロットを定義しているクラスを見たとき、
C1
よりも先導しているものがC2
のスーパークラスに存在していない場合です。
型の規則による結果は、スロットの値が関連するスロットの 各スロット指定子の型の条件を満たすことです。 スロットの型の条件が守られていない値を スロットに格納しようとした際の結果は未定義なので、 スロットの値は型の条件の安全性を失うでしょう。
スロットオプション:reader
, :writer
, :accessor
は、
スロットの特性を宣言すると言うよりは、
メソッドを作成するものです。
reader
とwriter
メソッドは、
7.6.7 メソッドの継承で説明される定義により継承されます。
スロットにアクセスするメソッドは、スロットの名前と、 スロットの値の型のみを使用します。 例えば、スーパークラスが、 指定した名前により共有スロットにアクセスすることを期待するメソッドを提供した場合、 またサブクラスが同じ名前で局所スロットを定義した場合を考えます。 もしスーパークラスによって提供されたメソッドを、 サブクラスのインスタンス上で使用した場合、 メソッドは局所スロットにアクセスします。
7.6 ジェネリック関数とメソッド
7.6章は7章Objectsのジェネリック関数関連の和訳を参照。
subtypep実装の基本
正直、あっているかどうかわからないのですが、
確認のために記載していこうと思います。
今回の投稿だけでは終わらないので、気が向いたら続きを書きます。
subtypep
の使い方
subtypep
とは、左の型が右の型に含まれているかどうかを確認する関数です。
こんな感じで覚えておけばいいと思います。
(subtypep 小 大) のとき t
正確には等しい時もt
となります。
例をあげます。
;; integerはrealに含まれる。 (subtypep 'integer 'real) →t; t ;; stringはstringに含まれる(同じなので) (subtypep 'string 'string) →t; t
返却値は二つありますが、第二返却値がnil
の場合は
判断できなかったという意味になります。
型satisfies
を用いると、たぶん大体はnil; nil
となります。
(subtypep 'integer '(satisfies hello)) →nil; nil
実験では、hello
という関数が存在しなくてもnil; nil
が返却されました。
つまり関数を実行すらしていないということです。
なお、第二返却値がnil
の場合は、必ず第一返却値はnil
です。
t; nil
のような返却のパターンはありません。
よって、返却値は下記の3通りとなります。
第1 | 第2 | 意味 |
---|---|---|
t |
t |
含まれる |
nil |
t |
含まれない |
nil |
nil |
わからない |
subtypep
の実装はとても難しいです。
理由は、実数の範囲指定、and
、or
、not
があるからです。
範囲指定とは例えば次のようなことを言います。
(subtypep '(integer 10 20) '(real * 40)) →t; t
整数10~20
は、実数40
以下に含まれます、という回答になります。
and
, or
, not
はそのままなので、
あらためて説明する必要はないかと思います。
例えばこんな感じ。
(subtypep 'integer '(not character)) →t; t (subtypep '(integer 30 80) '(or (real 10 50) (real 40 100))) →t; t
範囲指定とand
, or
, not
を一気に説明するのは無理なので、
今回はnot
から説明していきます。
subtypep
のnot
型not
とは、その名の通り反対を意味します。
subtypep
にnot
が含まれる場合はどうなるかわかるでしょうか?
例えば下記の例を考えます。
(subtypep 'integer 'real) →t; t
含まれる場合は、not
にすると、返却値はたぶんnil
になるのではないでしょうか。
(subtypep '(not integer) 'real) →nil; t
あっていました。
じゃあ右をnot
にすると?
(subtypep 'integer '(not real)) →nil; t
こっちもあっていますね。
では次の例を考えましょう。
(subtypep 'string 'real) →nil; t
元々の返却値がnil
の場合は、否定したらt
になるのでしょうか?
(subtypep '(not string) 'real) →nil; t
なりませんでした。
でも右をnot
にするとt
になります。
(subtypep 'string '(not real)) →t; t
じゃあ、もともとnil
の場合は、右をnot
にすると返却値は必ずt
になるのか?
と言うと、そうでもありません。
(subtypep 'real 'integer) →nil; t (subtypep 'real '(not integer)) →nil; t
どういうこと?
これを理解するには、型の範囲をちゃんと考えなければいけません。
次の2例を考えます。
(subtypep 'string 'real) →nil; t (subtypep 'real 'integer) →nil; t
どちらも返却はnil
ですが、分けて考える必要があります。
上のstring
とreal
は、重複することが全くない、完全なる排他です。
しかし下のreal
とinteger
は、重複する部分があるものです。
両者が完全に排他の場合、右を否定した場合のみt
となります。
これは図にするとわかりやすいのではないでしょうか。
例えば、例として含まれる範囲を|****|
で表すとしましょう。
(subtypep 'string 'real) string: ------|****|---------------- real : -----------------|****|----- ★互いに疎で含まれない
分かってもらえるでしょうか。
横軸はてきとうです。
こんな感じだとして、|****|
の部分がお互いに含まれないと表現します。
real
のnot
を取ると次のようになります。
(subtypep 'string '(not real)) string : ------|****|---------------- (not real): *****************|----|***** ★|ここ|★が含まれる
こうすると、string
の|****|
の部分が、(not real)
に
完全に含まれています。
一方、real
とinteger
は次のようになります。
(subtypep 'real 'integer) real : ------|*****************|--- integer: -----------------|****|----- ★realの方が大きいのでnil
real
とinteger
は互いに疎ではなく、重複する部分があります。
よってnot
を取ると次のようになります。
(subtypep 'real '(not integer)) real : ------|*****************|--- (not integer): *****************|----|*****
(not integer)
はreal
を包括できていません。
こんなふうに、subtypep
では、nil
を返却する場合は、
排他的なのか、そうじゃないのかをちゃんと調べないと、
not
の場合に対応することができません。
subtypep
でnot
の有無を網羅すると、パターンは次の4通りになります。
(subtypep 'a 'b) (subtypep '(not a) 'b) (subtypep 'a '(not b)) (subtypep '(not a) '(not b))
このうちで、もともとnil
だったものがt
になるパターンは次の2通りです。
(subtypep 'a '(not b))
a
とb
が排他的の場合はt
(subtypep '(not a) '(not b))
a
がb
を含む場合はt
下の場合の、2つがどちらもnot
の判定は楽です。
a
とb
を逆にした(subtypep 'b 'a)
を調べればいいだけですから。
問題は排他的の場合です。
これは計算だったりアルゴリズムで算出できるものではなく、
実装する段階で、これこれのパターンは排他ですよという情報を
自分で返却するようにしなければなりません。
どうやって実装するのかというと、地道にif
文を並べて作りましょう。
subtypep
実装の基本の第一段階は、
not
, and
, or
が含まれない場合の関数subtypep!
を作成し、
次の4パターンを返却をさせることです。
(subtypep! 'a 'b) :true 含まれる t; tに対応 :false 含まれない nil; tに対応 :exclude 排他 nil; tに対応 :invalid わからない nil; nilに対応
では、:exclude
を考慮して、not
が現れたらt
とnil
を返却するような
subtypep!
を作ればいいのかと思うかもしれませんが、
それだと機能が足りません。
:exclude
を考慮して、:true
, :false
, :exclude
(と:invalid
)を返却するような
subtypep!
を作成するのです。
これらの値は、のちに作成するand
, or
でも使用します。
では、ひとまずand
, or
がない場合の
『not
ありsubtypep!
』のアルゴリズムを示します。
(subtypep! a b)
の場合 (★not
なしの場合)(subtypep! a b)
をそのまま返却
(subtypep! (not a) b)
の場合(subtypep! b a)
が (★逆です):true
なら:exclude
を返却:exclude
なら:false
を返却- その他ならそのまま返却
(subtypep! a (not b))
の場合(subtypep! a b)
が:true
なら:exclude
を返却:exclude
なら:true
を返却- その他ならそのまま返却
(subtypep! (not a) (not b))
の場合(subtypep! b a)
が (★逆です):true
なら:true
を返却:exclude
なら:false
を返却- その他ならそのまま返却
これでどうでしょうか。
eqlには3種類の意味がある
表題の通り、Common Lispでは下記の意味があります。
- 関数
eql
- 型
eql
eql-specializer
なんでこんなにあるんでしょうね。
兄弟分である、eq
、equal
、equalp
さんたちは1つの意味しかありませんよ?
それぞれ説明していきます。
関数eql
普通はこいつです。
2つのオブジェクトが等しいかどうかを調べるときに使います。
関数eq
より適当ではないものの、関数equal
みたいに
詳しく調査して欲しくないという位置づけだと思います。
厳密に定義されており、下記のどれかが当てはまっているならt
です。
eq
がt
の場合。- どちらも
number
であり、同じ型で同じ値の場合 - どちらも
character
であり、同じ文字の場合
1つめのeq
はそのままアドレスを比較します。
2つめは、同じ型が要求されているので、数値として等しいだけの場合はダメ。
* (eql 0.0 0) →nil * (eql 0.0 0.0) →t * (eql 0.0d0 0.0s0) →nil * (eql 0.0d0 0.0d0) →t
3つめはそのままです。
* (eql #\a #\a) →t * (eql #\a #\A) →nil
型eql
Type-specifier
と呼ばれるものであり、
型という中で、値がeql
かどうかを調査します。
例えば次の通り。
* (typep 10 '(eql 10)) →t * (typep (cons nil nil) `(eql ,(cons nil nil))) →nil
余談ですが、型を評価する際に、内部で別の形式に変形することが良くあります。
例えば
(or (integer 10 200) (integer 100 300))
を
(integer 10 300)
のように変えたりします。
型eql
の場合も
(eql 10)
を
(integer 10 10)
に変形することがあります。
だから何だってわけじゃないんですが。
eql-specializer
これはmethod
を定義するときに使うものです。
method
は、引数に値の特性を指定することができます。
例えば下記の通り。
(defmethod aaa ((a integer) (b string)) ...)
この場合は、第一引数a
にinteger
を、第二引数b
にstring
を指定しています。
注意して欲しいのは、このinteger
やstring
と記述しているのは、
「型」ではなく「class
およびeql-specializer
」です。
下記の例の引数は、型(eql 10)
ではなく、eql-specializer
の指定です。
(defmethod bbb ((a (eql 10))) ...)
何が違うんだとしか思えないでしょう。
integer
と書けて、(eql 10)
と書けるなら、型じゃないのか?
でも型じゃないんです。
型が指定できないので
(defmethod bbb ((a (satisfies oddp))) ...)
や
(defmethod bbb ((a (not (eql 10)))) ...)
はエラーです。
こいつもエラーなので注意。
(defmethod bbb ((a (real 10 20))) ...)
じゃあinteger
とstring
は型じゃないのか?
と思われるかもしれませんが、型じゃありません。
型のふりをしたclos
オブジェクトです。
clos
の世界とlisp
の世界をある程度似せるために、
symbol
だけで表記された標準の型については、
同じclass
が用意されている場合があります。
何が用意されているのかは処理系依存ですが、
find-class
関数を使うことで調査できます。
clisp
の例を示します。
(find-class 'integer nil) →#<BUILT-IN-CLASS INTEGER> (find-class 'atom nil) →nil
なお、eql-specializer
の引数は、
defmethod
宣言時に一度だけ評価されます。
method
が呼び出されるたびに評価されるわけではありません。
例を示します。
呼び出すたびに値が増えていく関数hello
を作成します。
(let ((value 0)) (defun hello () (incf value)))
generic-function
のaaa
を作成します。
(defgeneric aaa (arg))
関数hello
をeql-specializer
の引数として、method
を定義します。
;; ここで(hello)は1を返却 (defmethod aaa ((arg (eql (hello)))) :hello) →#<STANDARD-METHOD ((EQL 1))>
実行してみます。
;; eql-specializerが機能している (aaa 1) →:HELLO ;; 何度か実行する (aaa 1) →:HELLO (aaa 1) →:HELLO ;; 確認 (hello) →2 ;; エラー (aaa 2) →no-applicable-method
forward-referenced-classとは何か
そんなクラス知らねえと言われても仕方がないほどマイナーなものです。
こいつは、だいたいはmop関連のパッケージに隠れています。
ANSI Common Lispには制定されていませんが、
CLOSを構築する際に必要となるので標準みたいなものでしょう。
まだあんまり読めてないんですが、 The Art of the Metaobject Protocolっていう専門書に載ってないんですよね。 検索しても日本語の解説は出てこない。 Common Lisp自体が古いから仕方がないといえば仕方がない。 じゃあ自分が書くか。
処理系依存ですが、次のようにすると確認できます。
* (find-class 'sb-mop::forward-referenced-class) #<STANDARD-CLASS SB-MOP:FORWARD-REFERENCED-CLASS>
ccl
? (find-class 'ccl::forward-referenced-class) #<STANDARD-CLASS FORWARD-REFERENCED-CLASS>
> (find-class 'clos::forward-referenced-class) #<STANDARD-CLASS FORWARD-REFERENCED-CLASS>
これは一体何なのかというと、defclass
で先行して存在しないsuperclass
を指定したときに
仮決めとして指定されます。
クラスを先行して設定した例をもとに説明します。
(defclass aaa (bbb) ()) ;; ★まだbbbは存在しない (defclass bbb () ((hello :initform "zzz"))) (slot-value (make-instance 'aaa) 'hello)) →"zzz"
では内部でどうなっているのか見てみます。
モップを使いますので、sbclならsb-mop
をuse-package
してください。
* (use-package 'sb-mop)
なんか清掃用具みたいですね。
まずは綺麗な状態から、再びクラスaaa
を作成します。
* (defclass aaa (bbb) ()) ;; ★まだbbbは存在しない #<STANDARD-CLASS COMMON-LISP-USER::AAA>
この状態でsuperclassの内容を確認します。
* (class-direct-superclasses (find-class 'aaa)) (#<FORWARD-REFERENCED-CLASS COMMON-LISP-USER::BBB>)
存在しないはずのクラスbbb
は、forward-referenced-class
と出てきています。
この表記から、forward-referenced-class
はmetaclass
だということがわかります。
ではクラスbbb
を作成します。
* (defclass bbb () ((hello :initform "zzz"))) #<STANDARD-CLASS COMMON-LISP-USER::BBB>
superclassはどうなっているでしょうか。
ここは処理系によって分かれました。
sbclの例を示します。
* (class-direct-superclasses (find-class 'aaa)) (#<STANDARD-CLASS COMMON-LISP-USER::BBB>)
class-of
がstandard-class
に変わっています。
この時点で確定するってすごいですね。
どうやっているんだろう。
bbb
をfind-class
にreferenced-class
として登録していたのか、
あるいはclass-direct-superclasses
の実行契機で見直したのか。
cclも確定していました。
? (class-direct-superclasses (find-class 'aaa)) (#<STANDARD-CLASS BBB>)
clispはまだ保留中です。
> (class-direct-superclasses (find-class 'aaa)) (#<FORWARD-REFERENCED-CLASS BBB>)
いずれにせよ、finalizeした時点で確定します。
classをfinalizeさせます。
> (make-instance 'aaa) #<AAA #x000801C57458>
あらためてsuperclassを確認します。
> (class-direct-superclasses (find-class 'aaa)) (#<STANDARD-CLASS BBB>)
なるほど。
ということは、forward-referenced-class
を持っている場合は
当然class-precedence-list
は作成できない?
[1]> (defclass aaa (bbb) ()) ;; ★まだbbbは存在しない #<STANDARD-CLASS AAA :INCOMPLETE> [2]> (class-precedence-list (find-class 'aaa)) *** - The class #<STANDARD-CLASS AAA :INCOMPLETE> has not yet been finalized. The following restarts are available: ABORT :R1 Abort main loop Break 1 [3]>
finalizeしないとダメだそうです。
finalizeがされたかどうかの確認は次のようになります。
> (class-finalized-p (find-class 'aaa)) NIL
ちなみにclass-precedence-list
の実行結果は、
でした。
まあどれでもいいんじゃないでしょうか。
不完全なクラスはdefclass
やensure-class
では作成を途中で放棄し、
最大限まで仕事を遅らせて、make-instance
なんかでfinalizeする必要が生じたとき、
ようやく重い腰をあげることがわかりました。
めんどくさい!
Npt Lispの紹介
Nptとは小さいLisp処理系です。
現在開発中です。
本ブログは、NptというLispの開発をするにあたり、
難しかったり引っかかった部分をブログに記載して行くことで、
実装の確認をあらためてしていこうという目的がありました。
それなのにNptの説明が一切ないのもどうかと思うので、
せっかくなので簡単に説明します。
Nptは「C言語のみで作成できるANSI Common Lisp」を目指しています。
半分以上はできたのかなと思っています。
ちなみにCLtL2は対象外。
現時点でバージョンはv0.1.8
となっていますが、
もしANSI Common Lispの関数を全部実装できたら
不完全であっても強制的にv1.0.0
に格上げして、
後はひたすらテストとバグとりをして行くことを考えています。
開発方針
開発方針を示します。
短所となりえる特徴を示します
将来やってみたいこと
- Threadセーフにしたい
- sqlite3みたいにamalgamationをやってみたい
順に説明します。
C99でコンパイルできること
NptはもともとC言語に組み込んで使うことを考えていました。
いろんな環境で使えるようにC11ではなく、C99をベースに作成しています。
本当はC89にしようかと思ってたんですが、
ちょっときついなと思い、あきらめた経緯があります。
環境はFreeBSD、Linux (およびWindows、ANSI-C)
FreeBSDは単純に自分が使っているからであり、 Linuxはたぶんみなさんが使っているからだろうという理由です。 Gentoo Linuxで動作確認をしています。
Windowsの開発は、優先順位は落ちますが、
現時点ではコンパイルとテストコードの実施は確認しています。
開発環境はWindows 10 64bit、Visual Studio 2017を利用しています。
ANSI-C環境とは、C99標準ライブラリのみで利用できる制限付きANSI Common Lispのことです。
今は対象外としていますが、もしかしたら利用できるようにするかもしれません。
一応コンパイル時に-DLISP_ANSIとすることで利用できるのですが、
テストケースを対応させていないので、どこまで正しいかわかりません。
たぶん次のようにコンパイルすると、勝手にANSI-Cモードになります。
$ cc *.c -lm
確認してみます。
$ ./a.out (find-class 'standard-object) #<STANDARD-CLASS STANDARD-OBJECT> ^D $ ./a.out --version npt Version 0.1.8 ----- Memory size 64bit Fixnum size 64bit Lisp mode ANSI-C ★ここで確認できる Thread mode remove Version 0.1.8 Build information 2019/03/30-00:19:59 ----- Execute mode standalone Release mode release Degrade mode release -----
可能な限りANSI Common Lispの機能のみを実装する
使い方の一つとしてC言語の組み込みを目的としているため、 拡張機能はかなり制限すると思います。
とは言っても、nptは組み込み以外でも、 作者が自分自身で利用することを考えていますので、 不便にならないよう、何らかの方法で実装はしたいです。
もしかしたらforkして別の名前で開発するかもしれません。
利用に制限はない
ライセンスを緩く設定しています。
もし気に入ってくれたのなら、好きに使ってください。
機械語の翻訳をしない
コンパイル機能は実装しません。
開発方針で実装しないと決めているので、 仮に実装できたとしてもしません。
つまりcompiled-function
を作成する方法はないということです。
存在自体はしており、例えば関数car
なんかは
C言語で作成してあるので、compiled-function
です。
まだ実装してないのですが、関数compile
はinterpreted-function
を生成する予定です。
つまりeval
とほぼ同じです。
もし、IntelのCPU対応でコンパイル作りたい!なんて思い始めたら、 たぶんnptをforkして別プロダクトで作ると思います。
拡張機能は極力実装しない
GUI機能とかsocket通信機能とかは、たぶん実装しません。
これも同じですね。
作りたくなったら、forkします。
実行速度が遅い
現時点で最大の欠点です。
バグがあるとかだったら直せばいい話ですが、
こちらに関しては決定的な解決が思い浮かびません。
たぶん最適化を作っていくことである程度は改善できると思います。
利用方法によって、早い部分と遅い部分はあると思いますが、 単純にファイルを一文字ずつ読み込むプログラムを作ったところ、 sbclと比べてnptは10000倍時間がかかり、 clispと比べてnptは7倍時間がかかったという結果でした。
改善はしていきたいですが、遅い部分だと場合によっては使い物に ならないかもしれません。
開発が不安定
作者の生活が不安定なため、中断する可能性は十分あります。
Threadセーフにしたい
拡張機能の実装には消極的ですが、
自分としてはThread機能を実装したいという思いがあります。
もしかしたらThreadセーフだけでも採用するかもしれません。
sqlite3みたいにamalgamationをやってみたい
nptのソースファイルをまとめて数個のファイルにすることです。
例えば
みたいな2つのファイルにする配布形態も用意できたらいいなと思います。
現在の足りない部分
実装できていないANSI Common Lispの大きな機能は下記の通り。
- CLOSほぼ全部 ★現在開発中
structure
全部loop
マクロ全部- pretty printing全部
- 環境に関する関数
core
ファイルの読み書きfasl
ファイルの読み書き
大まかに列挙しましたが、細かい部分はその他にも穴が開いていたりします。
例えばadjust-array
の大半の機能だったり、関数isqrt
の存在だったり。
機能の欠落は「common_*.c」というファイルの最後を見ると 分かるかもしれません。
引数&keyの内容を指定しない場合を考える
例えば
(defmethod aaa (value &key) ...)
のように&key
だけを指定して、その後の変数を記載しない場合があります。
全くの無意味のように見えますが、下記2つの場合において意味があります。
- 続けて
&allow-other-keys
を指定した場合 defmethod
かdefgeneric
でエラー回避のため
それぞれ説明します。
続けて&allow-other-keys
を指定した場合
&allow-other-keys
の場合は簡単に説明できます。
例えば下記の通り。
(defun aaa (value &key &allow-other-keys) ...)
このようにすることで、関数aaa
の引数にkey-value
の形を要求させることができます。
関数aaa
を呼び出すことを考えましょう。
例えば、下記の場合は問題ないです。
(aaa 100 :aaa 200 'bbb 300)
しかし、key-value
の形になっていない場合はエラーです。
(aaa 100 :aaa 200 'bbb) →★エラー、valueが足りない (aaa 100 :aaa 200 300 400) →★エラーの可能性あり、keyは本来symbolじゃないとダメ
関数aaa
では、引数を指定した所でただ内容を切り捨てています。
そうではなく引数&key
に加えて、&rest
だったり
マクロなら&whole
や&body
と組み合わせるなら、
もっとちゃんとした使い方ができると思います。
(defun aaa (value &rest args &key &allow-other-keys) ...)
この例では、key-value
の形を要求していることに加えて、
その内容を&rest
にてargs
に格納しています。
defmethod
かdefgeneric
でエラー回避のため
ではdefmethod
かdefgeneric
の方はどうでしょうか。
defgeneric
とdefmethod
には、lambda-list
を設定するときに、
守らなければいけない条件がいくつかあります。
そのうちの一部を下記に示します。
defgeneric
に&rest
か&key
があるなら、defmethod
に&rest
か&key
が必要。defmethod
に&rest
か&key
があるなら、defgeneric
に&rest
か&key
が必要。
では下記の例を見てみましょう。
(defgeneric aaa (value &rest args))
defgeneric
の引数には&rest
が含まれているので、
defmethod
では&rest
か&key
が必要になります。
続けて次の例を見ていきます。
(defmethod aaa (value &key hello) ...)
上記のmethod
では&key
があるので問題なしです。
次はどうでしょうか。
(defmethod aaa (value) ...)
上記はエラーです。
defgeneric
に&rest
があるのに、method
には&rest
も&key
もありません。
では次の例はどうでしょうか。
(defmethod aaa (value &key) ...)
面白いことに、&key
があるから問題ないとみなされます。
(defmethod aaa (value) ;; ★エラー ...) (defmethod aaa (value &key) ;; ★問題なし ...)
上記2例は、受け取ることができる引数は全く同じであるにもかかわらず、
下の方だけが合法で、lambda-list
のエラーチェックを回避できるのです。
lambdaと記載できる場所はどこか
よく目にする(lambda ...)
という表記はかなり特殊なものであり、
マクロを抜きにするならば、限定された場所でしか記述できません。
マクロのlambda
とリードマクロの#'
があるため、
式の中で気軽に記載することができるのです。
マクロの例としてはこんな感じ。
(setq *call* (lambda () :hello))
このlambda
マクロは、次のように展開します。
(setq *call* #'(lambda () :hello))
さらにリードマクロ#'
より、次のように展開されます。
(余計なことを言うと、リードマクロの展開タイミングはここじゃないかもしれない)
(setq *call* (function (lambda () :hello)))
最終的にspecial operatorのfunction
の引数に展開されるわけです。
じゃあfunction
の引数のlambda
はマクロ展開されないのか?
されません。
function
の引数はquote
の引数と同じように特殊な場所であり、
評価対象ではないのでマクロ展開されないのです。
ではマクロのlambda
を考えない場合は、lambda
と記載できる場所はどこでしょうか?
答えは下記の通り。
function
の引数- 関数呼び出し時の関数名
function
の引数は上記の例で説明しました。
関数呼び出し時の関数名とは、普通に関数を呼び出すときのリストの第一要素の事です。
((lambda () :hello)) →:HELLO
これだとわかりづらいですよね。
((lambda (a b) (+ a a b b)) 10 20) →60
どうでしょうか。
次の実行とほぼ同じとなります。
(defun testplus (a b) (+ a a b b)) (testplus 10 20) →60
以上で、lambda
の記載できる場所の説明は終わりです。
function
と関数呼び出しの構文の違い
こっちが本題だったりしますが、
special operatorの(function name)
と、関数呼び出しの(name ...)
では、
どちらもsymbol
と(lambda ...)
を記載することができました。
両者は同じものを許容するのでしょうか?
実は違います。
(function name)
が許容するものは下記の通り。
symbol
(lambda ...)
(setf name)
それに対して、(name ...)
が許容するものは次の通り。
symbol
(lambda ...)
つまり関数呼び出しでは、(setf name)
を直接呼び出すことが許されていません。
これはちょっと意外でした。
【参考】http://www.lispworks.com/documentation/HyperSpec/Body/03_abab.htm
If the car of the compound form is not a symbol, then that car must be a lambda expression, in which case the compound form is a lambda form.
つまりは下記のコードは規約違反です。
(defun (setf aaa) (v c) (rplaca c v) v) (let ((c (cons 10 20))) ;; (setf (aaa c) 200) ((setf aaa) 200 c) ;; ★エラー (format t "~A~%" c))
面白いことに、clispではこのコードが全く問題なく動作します。
clispによる実行結果 (200 . 20)
setf
を直接呼び出したいならfuncall
/apply
を使いましょう。
(defun (setf aaa) (v c) (rplaca c v) v) (let ((c (cons 10 20))) ;; (setf (aaa c) 200) (funcall #'(setf aaa) 200 c) ;; ★問題なし (format t "~A~%" c))
実行結果
(200 . 20)
そんな馬鹿なことしてないでsetf
マクロ使えという声が聞こえてきそうですが、
私もそう思います。
なお、当たり前ですが、defsetf
かdefine-setf-expander
で定義されたものの場合は、
関数じゃないのでこの方法では呼び出しできません。