Lisp/基本からさらに一歩進んで/CLOS
Common Lisp Object System (CLOS) は Common Lisp のプログラムで使用することのできるオブジェクト指向のプログラミングテクニックの一機能です。 CLOS では オブジェクト, クラス, メソッド といった概念とそれらの相互作用について定義されています。 CLOS ではどんなプログラミング言語よりも強力なオブジェクトシステムが利用可能です。ですが、 CLOS に特有の側面について習熟するには時間がかかるでしょう。幸いなことに、 CLOS を使うのに CLOS の専門家になる必要はありません。
CLOS に関する2つの概念で クラス群 と ジェネリック関数 というのがあります。では最初の一つから見ていきましょう。
クラス群とメソッド
[編集]クラスは構造の記述と、その インスタンス(実体) の振る舞いを示した "雛形" です。 Lisp での全ての種類のデータは何かのクラスのインスタンスです。例えば、数値のクラスや文字列のクラスのような組み込みクラスがあります。class-of 関数を使うことで、 Lisp オブジェクトのクラスを判別することが出来ます。
(class-of 5) => ;#<BUILT-IN-CLASS INTEGER>
(class-of "aaaa") ;=> #<BUILT-IN-CLASS STRING>
実際に表示されている結果はあなたの使用している実装環境によって違うかもしれませんが、しかし考え方は同じです。#< >
は unreadable data のための Lisp の構文です(unreadable とは人間が読めないことを意味するのではなく、 Lisp リーダーが読み込めないということを意味します)。 5 が組み込みクラス integer のインスタンスであること、 "aaaa" が組み込みクラス string のインスタンスであることが容易に理解できます。
組み込みクラスは普通はあまり興味を引くものではないでしょうが、ユーザー定義のクラスを作成する方法もあります。ユーザー定義のクラスの作成は defclass マクロで行うことが出来ます。ユーザー定義クラスのそれぞれのインスタンスは、様々な Lisp のデータを含むことができるいくつもの slots を保持します。 defclass の典型的な使い方を見てみましょう。
(defclass book ()
((author :initarg :author :initform "" :accessor author)
(title :initarg :title :initform "" :accessor title)
(year :initarg :year :initform 0 :accessor year))
(:documentation "Describes a book"))
これは以下のような構造を持ちます。
(defclass name (superclasses)
(slots)
options)
今はスーパークラスについては無視します。とりあえずは代わりにスロットについて見ていきましょう。それぞれのスロットには名前といくつものオプションが付属しています。オプションは以下のようなものです。
:initarg
- クラスのインスタンスが作成されたときにスロットに値を与えるときに使用される keyword:initform
- もしスロットのための何の値も与えられない場合は、 initform が評価された結果に初期化されます。 initform がなければエラーが返されます。:reader
- 関数に特定のスロットを読み込むように指定します。:reader aaa
の意味するところは、 aaa という関数を作成し、 aaa のインスタンスはスロットの値を返すようにしろ、ということです。:writer
- 関数に特定のスロットに書き込むように指定します。:writer bbb
の意味するところは、 bbb という関数を作成し、 bbb の値のインスタンスは、インスタンスのスロットに値をセットしろ、ということです。:accessor
- 関数にスロットの値を読み書きするように指定します。:accessor foo
の意味するところは、 foo という関数を作成し、さらに (setf foo) という関数を作成し、そして (foo instance) でスロットの値を読み込み、そして (setf (foo instance) value) でスロットの値をセットします。- Node:
:reader foo :writer foo
は異なります。 reader と writer の名前は違うものにしなくてはなりません。代わりに:accessor
を使用してください。
- Node:
:documentation
- 特定のスロットに文書文字列を作成します。
全てのオプションは完全に任意ですが、しかし少なくとも一つの :initarg
か、あるいは :initform
はオブジェクトが作成されたときにスロットの初期化が可能なように、与えられなければなりません。失敗するとランタイムエラーを返します。
クラスのオプションとして、唯一便利なのが :documentation
です。これは全てのクラスに文書文字列を提供します。
ではもうクラスのことがわかったので、クラスのインスタンスが作れるはずです。 Lisp の全てはオブジェクトです。しかし上記の defclass で定義されたような一般クラスのインスタンスは、標準オブジェクトと呼ばれます。この項の残りのワードオブジェクトは標準オブジェクトのことです。
新しいオブジェクトを作るにはどうしたらいいのでしょうか? make-instance 関数がこの要求に沿ったものになります。
(setf *my-book* (make-instance 'book))
これでシンボル *my-book* の値はクラス book のオブジェクトとなりました。 make-instance の最初の引数は、それが class-of で呼び出した結果のように、クラスそのものかどうか、あるいは、そのシンボルがシンボルをクオートした結果のように、それがクラスの名前かどうかを評価します。このケースではシンボルを使用しましたが、これがオブジェクトを作成するにはより簡単です。
ではシンボル *my-book* を見てみましょう。
(class-of *my-book*) ;=> #<STANDARD-CLASS BOOK>
(author *my-book*) ;=> ""
(year *my-book*) ;=> 0
この wikibooks は今はまだ面白みに書けますね。このオブジェクトのフィールドはデフォルトクラスの値にセットされますが、これを変更するのは簡単です。
(setf (title *my-book*) "ANSI Common Lisp")
(setf (author *my-book*) "Paul Graham")
(setf (year *my-book*) 1995)
これは適切なアクセサ関数がクラス定義で用意されるからなのですが、一般的なケースでのオブジェクトのスロットへのアクセスはいくぶん難しくなっています。例えば、もし読み込み関数だけが定義されていて、まだスロットの変更が可能な場合は slot-value 関数を使います。
(setf (slot-value *my-book* 'year) 1995)
(year *my-book*) ;=> 1995
同様に読み込み関数を捨てて slot-value 関数でスロットの値を読むこともできますが、しかし、これはコードの可読性を減らすことになるでしょう。
ほとんどの場合は作成したオブジェクトの全てのスロットにデフォルトの値を設定して欲しくはないでしょう。例えば、特定の本を表すオブジェクトを作成したかったとします。あなたは既にその本のタイトル、著者、そして出版年を知っていて、新しいオブジェクトを役に立たない空の値ではなく、これらの値で初期化したいとします。もしスロット定義で指定された :initarg オプションがあるのなら、そのスロットはオブジェクトの作成の際にユーザー指定の値に初期化することが出来ます。これは make-instance に同様のキーワード引数を与えることで可能になります。
(make-instance 'book
:author "Paul Graham"
:title "ANSI Common Lisp"
:year 1995)
book のスロットのデフォルトの値はほとんど役に立たないので、( 0 年という年はありませんし、空のタイトルの本もありません。) :initform スロットオプションは削除されなければなりません。それでスロットの初期値を入れ忘れたユーザーはエラーをもらうことになり、ユーザーが次にスロットを作成するときには必要な情報をスロットに与えることでエラーを修正しようとするでしょう。変更をしたくないスロットに :reader 関数だけを与えるのも賢いやり方です(今回の book クラスでは全てがこの「変更したくない」ケースに当たりますね。)。短く言えば、これらのオプションを単体で付与するのは親切な設計にもなりうるということです。