K.Sasada's Home Page

Tiny CLOS 入門 - Tiny CLOS Tutorial


http://home.adelphi.edu/~sbloch/class/272/tclos/tutorial.shtml を翻訳したものです。まだ公開する許可を貰っていないので見ないでください。

ついでにまだ全部訳せてないです。

一応、許可は頂いたんですが、原文が見つからない。


本稿は Scheme についてよく知ってるってことを前提としていますが、、もしまだ知らないのなら、先に Scheme Tutorialを読んでください。

I assume you're already reasonably familiar with the Scheme language; if not, please read the Scheme Tutorial first.

読みましたか? はい。

Are you back now? Good.

紹介 - Introduction

CLOS というのは、Common Lisp Object System の頭文字をとったもので、Lispでオブジェクト指向プログラミングを行う人のため、Common Lispに拡張された標準的なセットです。Scheme とは知ってのとおり、Lispの方言で、Common Lispの文法より単純で一貫性のある文法になってます。Tiny CLOS は、Scheme用の CLOS で、1992年に Gregor Kiczales によって書かれました。構文的には CLOS と違いますが、基本的なOOPへのアプローチは CLOS のそれと一緒です。

CLOS, an acronym for Common Lisp Object System, is a standard set of extensions to the Common Lisp language to help people do object-oriented programming in Lisp. Scheme, as you know, is a dialect of Lisp with a simpler, more consistent syntax than Common Lisp's. Tiny CLOS is a Scheme version of CLOS written in 1992 by Gregor Kiczales. It differs from CLOS syntactically, but the basic approach to OOP is the same as in CLOS.

どうやって Tiny CLOS を使うか - How to Use Tiny CLOS

Tiny CLOS は Scheme で書かれています。ソースコードは、本当に見たいんだったら、tiny-clos.scm というファイルにあります。ですが、オブジェクトやクラスシステムのためのブートストラップのために、えらい複雑です。理解するのが楽なバージョンは(関数型じゃないんだけど)、tiny-rpp.text にあります。他の便利なサポートコードは同じディレクトリにあります。

Tiny CLOS is written in Scheme. The source code, if you really want to look at it, is in the file tiny-clos.scm, but it has a lot of complications due to the need to bootstrap the object and class system; an easier to understand (but not functional) version is in tiny-rpp.text. Other useful support code is in the rest of this directory.

しかし、ソースコードなんて要りません。もっと便利なのは、tclos コマンドを使うことです。これは、単純なシェルスクリプトで、 MIT Scheme を Tiny CLOS をロードした状態のメモリイメージとともに起動するものです。これらの意味がわからなかったら、無視してください。ただ、tclos ってのを使えばいいんですよ。

However, you don't need to use the source code: much more convenient is to use the command tclos, a simple shell script that runs MIT Scheme with a memory image that has Tiny CLOS already loaded. If you don't know what all that means, ignore it; just use the command tclos.

CLOS vs. 他のOOPのアプローチ - CLOS vs. other approaches to OOP

今日、もっとも人気のあるオブジェクト指向言語(例えば C++ とか Java)は、多くの構文や考え方を共有してます。CLOS や Tiny CLOS は Lispっぽい構文で、上述した言語のような、ブロック構造の構文とは全然違います。これらはランタイムでの多態(polymorphism) という考え方(すなわち、関数はいくつか異なる方法で動くけど、それは適用されるオブジェクトの種類に依存してるってこと)とか、継承とか、色々と基本的な全てのほかのOOな言語と共有してます。また、ほとんどのOOな言語のように、CLOSやTiny CLOS はすべてのオブジェクトクラス(ひとつ以上のスーパークラスによって書かれているんでしょう)の要素であると考えます。

The most popular object-oriented languages today (e.g. C++ and Java) share much of their syntax and much of their philosophy. CLOS and Tiny CLOS have a Lisp-like syntax, very different from the block-structured syntax of the other languages mentioned above. They share the notions of run-time polymorphism (i.e. a function that works in several different ways depending on the kinds of objects to which it is applied), inheritance, etc. with essentially all other OO languages. And like most OO languages, CLOS and Tiny CLOS consider every "object" to be an element of a "class", which may be written in terms of one or more "superclasses".

CLOSと上述した他の言語との一番顕著な違いは CLOS(や Tiny CLOS)では、多態関数は普通の関数、クラスのオブジェクトに縛られていないもののように見え、そのように振舞うことです。反対に、すべてのC++やJavaの多態関数はどれかひとつのクラスに属しており、そのクラスのインスタンスにあった(訳注:メソッドが)起動されます。たとえば、dial という名前のクラスと、多態関数 turn があったとして、ThisDial という dial (訳注:クラスのインスタンス)を 200 にセットして turn したいとします。C++ プログラマはこんなふうに書きます。

The most significant difference between CLOS and the other languages mentioned above is that in CLOS (and Tiny CLOS), a polymorphic function looks and behaves like an ordinary function, not tied to any one class of objects. By contrast, every polymorphic function in C++, Java, et al ``belongs'' to one particular class, and must be invoked in conjunction with an instance of that class. For example, suppose there were a class named dial and a polymorphic function named turn, and we wished to turn the dial ThisDial to setting 200. A C++ programmer would write

ThisDial.turn (200); 

一方、CLOS プログラマはこう書きます。

while a CLOS programmer would write

(turn ThisDial 200) 

違いは単に構文的なものではありません。C++ では、複数のオブジェクトを伴う動作は、パラメータの残りとしてそれらの中の一つのオブジェクトに属していなければなりません。別の例で言うと、2つのクラス、LightBulb(電球)とsocket(ソケット)(訳注:クラス名が大文字じゃないって違和感ある。rubyに毒されてるなぁ)があったとします。そして、(その中でも)電球をソケットに付けるような多態関数を書きたかったとします。C++(やJavaとか)では、プログラマが電球にメソッドをつけて、ソケットをパラメータとして受け取るようにするか、その逆(ソケットにメソッドをつけて(訳注:韻を踏んでるなぁ)、電球をパラメータとしてとるよう)にするか選ばなくちゃいけません。

The distinction is not merely syntactic: in C++, an action that involves multiple objects must "belong" to one of them, with the rest as parameters. For another example, suppose there are two classes named LightBulb and socket, and we wish to write a polymorphic function that (among other things) puts a light bulb into a socket. In C++ (or Java or ...), the programmer must choose whether to write a method for light bulbs, taking a socket as a parameter, or a method for sockets, taking a light bulb as a parameter:

ThisBulb.PutIn (ThatSocket); 
ThatSocket.PutIn (ThisBulb); 

それに対して、CLOS プログラマは単純にこう書きます。

whereas the CLOS programmer simply writes

(PutIn ThisBulb ThatSocket) 

結局、多態関数(CLOS用語では総称関数(generic function)という)はいくつかの定義がある普通の関数で、実行時に自動的に引数のクラスに一番ぴったりくる定義が選ばれます。どのクラスのオブジェクトにそのメソッドを適用するかは誰も言及せず、また C++ が作った friend が少し必要になります。これは、他のクラスであるにも関わらず、そのクラスの private な情報にアクセスすることです。

In effect, a polymorphic function (called a "generic function" in CLOS terminology) is an ordinary function with multiple definitions, which automatically chooses the most appropriate definition at run time based on the classes of its arguments. No one argument is singled out as "the object" to which the method applies, and there is little need for the C++ construct called a "friend", a function applied to an object of one class which nonetheless has access to the private information of objects of another class.

この選択は上記で指摘したいくつかの利点があります。また、不利な点もあります。メソッドはある一つのクラスに属すわけではなく、いくつかのクラスの組み合わせに属します。これではメソッドの可視性をコントロールするのが難しく、C++ でいう public/protected/private 識別子をあてはめることができません。この欠点が利点を上回っているかどうかは個人的な、殆ど宗教的な判定でしょう。この点に関するこれ以上の議論については OOP FAQ part 1, item 1.19 を参照してください。

This choice has several advantages, as pointed out above. It also has disadvantages: since a method belongs not to one specific class but to a combination of classes, it is much more difficult to control the visibility of methods, and the public/protected/private distinction in C++ cannot be applied to methods. Whether you consider these disadvantages to outweigh the advantages is a personal, almost religious, decision. For more discussion of this issue, see the OOP FAQ part 1, item 1.19.

CLOS のクラスとオブジェクト - Classes and Objects in CLOS

CLOSでは、ほとんどのオブジェクト指向言語のように、すべての オブジェクト(object)は一つ以上のクラスの要素で、その定義は基底クラス(superclasses)から派生したものです。このオブジェクトの挙動はそのクラスにより決定されます。挙動 というのは次の点に言及します。

In CLOS, as in most object-oriented languages, every "object" is an element of one or more "classes", whose definitions may be derived from the definitions of other "superclasses". The behavior of an object is determined by its class(es): here "behavior" refers to

慣例により、クラスの名前は山形括弧で囲みます。たとえば <object> とか <person> とか。(訳注:Scheme とか Lisp って、関数名などのシンボルにいろんな記号使えます。使えない記号のほうが少ないです)

By convention, class names in CLOS are surrounded in angle-brackets, e.g. <object>, <person>.

インスタンスの生成 - Creating instances

あるクラスのインスタンスを作るには、make 関数を使います。

To create a new instance of an existing class, use the make function:

(define sam (make <person>)) 

これは、<person> クラスのインスタンスを作って、Scheme の変数 sam でそれ(訳注:作ったインスタンス)と束縛します(訳注:Schemeなので、代入じゃないです)。いくつかのクラスは次のように定義されます(以下の初期化を見てください)。新しいインスタンスの属性を設定するための追加の引数が指定できます(そのインスタンス変数を初期化するとか)。たとえば、<person>クラスは次のように定義します。

creates a new instance of the <person> class and binds the Scheme variable sam to it. Some classes are defined in such a way (see the initialize function below) that additional arguments can be provided to the make function to determine properties of the new instance, e.g. to initialize its instance variables. For example, one might define the <person> class in such a manner that

(define sam (make <person> 'age 38))

これは新しい <person> のインスタンス変数 age を 38 で初期化します。

would initialize an instance variable named age of the new <person> to 38.

クラスの作成 - Creating classes

新しいクラスの作成は、単純にすでに定義したクラス(<class>)のインスタンスを作ることです。たとえば、

Creating a new class can be seen as simply creating a new instance of the predefined class <class>, e.g.

(define <person> (make <class>
                       'direct-supers (list <object>)
                       'direct-slots (list 'name 'age)))

これは、新しいクラスを作り、直接の基底クラスのリストとして1要素のリスト (<object>) と、直接のスロットのリスト として(name age) で初期化します。しかし、新しいクラスの作成というのは、そういう一般的な操作で、 make-class 関数として提供されています。二つの引数、(i) 直接の基底クラス(とくに多重継承をしないなら一つだけ)と (ii) スロット、またはインスタンス変数の名前のリストをとります。すべてのScheme の変数のように、これらの変数は型付けされていません。つまり、数 38 やシンボル 'bluebird やリスト (red green blue) や、他の Scheme (または Tiny CLOS)のオブジェクトを関連付けることができます。だから、もっと一般的な 上で述べた<person> クラスの作り方は次のようになります。

creates a new class, initializing its "list of direct superclasses" to the one-element list (<object>) and its "list of direct slots" to (name age). However, creating a new class is such a common operation that they've provided a special make-class function to do the job. It takes two arguments: the list of direct superclasses (typically only one, until you start playing with multiple inheritance) and the list of names of "slots", or instance variables. Like all Scheme variables, these variables are untyped: you can equally well plug in the number 38, the symbol bluebird, the list (red green blue), or any other Scheme (or Tiny CLOS) object. So the more common way to create the <person> class above would be

(define <person> (make-class (list <object>) (list 'name 'age))) 

インスタンス変数 - Instance Variables

OOP ではふつう、インスタンス変数という、クラスのインスタンスにそれぞれ関連付けられている変数というのがあります。上の例では、name と age は <person> のインスタンス変数です。なぜなら、人にはそれぞれ自分の(できるかぎり別々の)名前と歳があるからです。インスタンス変数は、PascalのレコードやCの構造体のフィールドより大きく、よりよいバージョンに見えるでしょう。CLOS用語では、インスタンス変数は スロット(slot) と言います。

In OOP in general, an "instance variable" is a variable associated with each individual instance of a class. In the above example, name and age are instance variables of the class <person> because each person has its own (possibly distinct) name and age. Instance variables may be viewed as a bigger, better version of the fields in a Pascal record or a C struct. The CLOS word for instance variable is "slot".

特定のオブジェクトの特定のスロットの値を得るためには slot-ref 関数を使います。これは、二つの引数、つまり問題のオブジェクトとスロットの名前を取り、そのスロットの値を返します。

To get the value of a specified slot in a specified object, use the slot-ref function, which takes two parameters -- the object in question, and the name of the slot -- and returns the value of the slot:

(slot-ref sam 'age) 
38 

特定のオブジェクトの特定のスロットについて、その値を変えたい場合は slot-set! 関数を使います。関数名の最後のビックリマークに注目してください。これは(殆どの Scheme 関数とは違って)これはその引数を実際に変更することを示しています。これは三つのパラメータ、オブジェクト、スロット名、そして新しい値をとります。もし Sam の誕生日だったらこう書くわけです。

To change the value of a specified slot in a specified object, use the slot-set! function. Notice the exclamation point at the end of the function name, indicating that (unlike most Scheme functions) this one actually changes its arguments. It takes three parameters: an object, a slot name, and the new value. So if it were Sam's birthday, we might write

(slot-set! sam 'age (+ 1 (slot-ref sam 'age))) 

スロット名を実装として扱うことは、インターフェースとして扱うよりも、通常よいプログラミングの習慣だと考えられています。なので、あなたの作ったオブジェクトクラスのユーザは、そのオブジェクト内部のスロットに直接アクセスしません。これは、二つの理由によります。(1) もしユーザが特定の名前のスロットに依存しはじめると、インスタンス変数の名前の変更や削除の自由がなくなります。(2) また、オブジェクトはいくつかの関連した変更すべきでない情報を保持するだろうが、原則的に、見てないうちにユーザにそのどれかの変更をしようとするのをやめさせる方法はありません。だから、殆どの CLOS クラスは アクセス関数(access function) を用意します。これは、どのようにその情報が格納されているかを知る必要なく(スロットの名前や、どんなのがスロットに格納されているかということ)引き出すことができます。他の種類のアクセス関数は、オブジェクトについての正しい情報の変更を、繰り返しますがどのように情報が格納されているかということ知る事なしに可能にします。さてと・・・。

It is generally considered good programming practice to treat slot names as implementation, rather than interface, so users of your object class don't access slots in your objects directly. This is for two reasons: first, if users start relying on your objects to contain slots with specific names, you lose the freedom to change the implementation by renaming or even eliminating some of those instance variables; and second, an object may contain several pieces of related information that must be kept consistent, an essentially impossible task if users can change one piece of information at a time behind your back. Accordingly, most CLOS classes are provided with "access functions" whose purpose is simply to give the user certain information about the object, without the user ever knowing how that information is stored (the slot name, or even whether it is stored in a slot at all). Another kind of access function allows the user to change certain information about an object, again without knowing how that information is stored. Which brings us to...

総称関数とメソッド - Generic functions and methods

多態は総称関数と呼ばれている何かで実現されています。これは、(潜在的に)いくつか違った定義(メソッド)をそなえており、実行時にその引数によってひとつ選択されます。

Polymorphism is provided in CLOS by something called a generic function: a function with (potentially) several different definitions (methods), one of which is chosen at run-time based on the classes of the arguments.

総称関数の作成 - Creating generic functions

CLOS では make-generic 関数によって新しい総称関数を作ることができます。これは引数をとりません。

You can create a new generic function in CLOS with the make-generic function, which takes no arguments:

(define turn (make-generic)) 

add-method によって、メソッドを総称関数に追加することができます。こちらのほうが利用頻度は高いでしょう。実際、まだ総称関数として make-generic で定義してない総称関数に add-method を呼んだ場合、それを定義するため、 make-generic は結局必要ではありません。

You'll probably use the function add-method, which adds a method to an existing generic, much more often. Indeed, if you apply add-method to a generic that hasn't already been defined with make-generic, it'll define it for you, so you actually never need make-generic at all.

(add-method turn this-method) 

メソッドの作成と追加 - Creating and attaching methods

さて、make-generic か add-method によって総称関数が作れるようになりました。また、(後者では)新しいメソッド(method) をそれに追加することができます。しかし、メソッドってなんでしょう? 早い話、メソッドは普通の Scheme 関数定義に、どのメソッドを適用すればいいかを知るための、どのクラスがどの引数になければならないかという情報を加えたものです。メソッドは make-method という関数で作られます。make-method は二つの引数をとります。ひとつはクラスのリスト、もうひとつは関数(例によって lambda式で)です。例です。

OK, so you can use make-generic or add-method to create a generic function, and (in the latter case) attach a new "method" to it. But what is a "method"? In a nutshell, a method is an ordinary Scheme function definition, together with information indicating what classes which arguments have to belong to in order for this method to be applicable. Methods are constructed by a function named make-method, which takes two arguments, a list of classes and a function (typically presented as a lambda-form). For example,

(define this-method
        (make-method (list <dial> <number>)
                     (lambda (cnm dial setting) 
                          (while (< (position-of dial) setting)
                                 (turn-up dial)))))

ここで、このメソッドは最初の引数のクラスが <dial> で、次が <number> である場合に呼ばれます。本体は lambda 式で、二つの引数 dial と setting を取り、希望の設定になるかそれをこえるまでダイアルを回します(事前に position-of と turn-up はどこかに書いてあるものとします)。(訳注:turn-up ってどう訳すんだろう)。

Here we've defined a method which applies whenever the first argument is a <dial> and the second is a number. Its body is the lambda-form that takes two arguments named, dial and setting, and repeatedly turns up the dial until its position matches or exceeds the desired setting. (I assume that position-of and turn-up are already written somewhere.)

多分、説明していない cnm という、上の lambda式で出てきたパラメータが気になるでしょう。殆どのオブジェクト指向言語では、スーパークラスとほとんど同じだけど、それに少し前処理や後処理を加えたりすることが(普通)できます。CLOS ではスーパークラスのメソッドを書き直す必要がないように、スーパークラスのメソッドを呼び出す方法があります。Tiny CLOS の実装では、メソッドが呼ばれたときには最初のパラメータに call-next-method というものが渡され、単純にそれを呼び出すことで同じ generic の基底クラスのメソッドを呼び出すことができる(訳注:いい加減)。不幸なことに、call-next-method 機能を利用しようとは思わなくても、各メソッドはこのように余分な最初の引数を持たなければなりません。どのように使うかは後述します。

You have no doubt noticed the unexplained "cnm" parameter to the lambda-form above. In most object-oriented languages, it is possible (and common) for a method to do almost the same thing as the corresponding method for a superclass, but with a little extra work before or after. To avoid having to rewrite all the code from the superclass's method, CLOS provides a way to invoke the superclass's method for the same generic. The Tiny CLOS implementation of this is, whenever a method is invoked, to give it a function named "call-next-method" as its first parameter, so that if it wishes to invoke the superclass's method for the same generic, it can do so by simply calling call-next-method. It is perhaps unfortunate that, even if you don't intend to use the call-next-method mechanism, every method must take an extra first parameter just in case. We'll discuss how to use this mechanism later.

練習で、このメソッドを総称関数 turn に追加で悩むことはないでしょう。こうやります。

In practice, you probably wouldn't bother defining this-method and then adding it to the generic turn; instead, you would probably do both in one step:

(add-method turn 
        (make-method (list <dial> <number>)
                     (lambda (cnm dial setting) 
                          (while (< (position-of dial) setting)
                                 (turn-up dial)))))

総称の初期化 - The initialize generic

たくさんのオブジェクト指向システムではたくさんの既に定義されたクラスとメソッドがついており、ユーザが必要に応じてそれらのサブクラスを作ってメソッドをオーバーライドすることを期待します。一つの例として、generic の初期化、つまりクラスのインスタンスが生成されたときに自動的に呼ばれるものがあります。初期化の最初の引数は生成中のオブジェクトで、2番目は作成時に与えられた残りの引数です。(訳注:最初の引数は cnm じゃん。だから一個ずつずれる)

Many object-oriented systems come with a variety of classes and methods already defined, and expect the user to create subclasses and override those methods as need be. One example is the initialize generic, which is called automatically whenever make creates a new instance of a class. The first argument to initialize is the object being created, and the second is a list of any extra arguments that were given to make.

混乱しますね。ここに例があります。上記のような <person> クラスを作るとしましょう。そして、<person>オブジェクト作成と同時に人の名前(と、任意で歳)を与えることにしましょう。こう書きます。

I know that's a little confusing; here's an example that may help. Suppose we've defined the <person> class as above, and we want to provide a person's name (and, optionally, age) at the same time we create a <person> object. We might write

(add-method initialize
    (make-method (list <person>)
        (lambda (cnm obj initargs)
            (slot-set! obj 'name (car initargs))
            (unless (null (cdr initargs))
                    (slot-set! obj 'age (cadr initargs))))))

ここで、こう書くと "Sam" という人が作れます。

Now we can create a person named "Sam" by typing

(define friend1 (make <person> "Sam")) 

そして、別の25歳の "Jeff" をこう書けば作れます。

and another, 25-year-old, person named "Jeff" by typing

(define friend2 (make <person> "Jeff" 25)) 

初期化のための initialize という名前の slot の generic はよく使うため、<object> クラスのサブクラス <init-object> を作りました。これは、初期化メソッドがスロット名と値のリストをとります。initargs.scm で(このとても単純な)ソースを見てください。典型的な使い方は次のようになります。

Since a fairly common use of the initialize generic is simply to initialize named slots, I've written a subclass of <object> named <init-object> whose initialize method accepts a list of slot names and values; see initargs.scm for the (fairly simple) source code. A typical use would be

(define <person> (make-class (list <init-object>) (list 'name 'age)))
(define pal (make <person> 'name "Jane" 'age 46))

これはユーザはslot名を知らないべきであるという原則に違反しています。<init-object> クラスは、単に解説する例を作るのを便利にするためのものです。

This violates the principle that users shouldn't know the names of slots; the <init-object> class is just a convenience for constructing illustrative examples quickly.

例 - An example

さあ、完璧な例を試してみましょう。ロボットアームにダイアルを回すよう伝える仮の関数なんて必要ないような例でね。取り組む問題は、ニューヨークの仮の大学(訳注:この記事はAdelphi Universityの授業用らしいです)の、学生がどのコースを登録したかを把握するというものです。この問題の説明には2つの明らかなオブジェクト、studentcourse があるので、それらのクラスを作りましょう。初期化を簡単にするために、<init-object> のサブクラスとします。実際、<person>クラスはすでに<init-class> のサブクラスで、name と age をもってます。<student> を <person> クラスのサブクラスとして、まだ <person> クラスにない情報とメソッドを足して作りましょう。

Let's try a more complete example, one that doesn't require hypothetical functions to tell a robot arm to turn a dial. The problem at hand is to keep track of a collection of students, each of whom is registered for various courses at a hypothetical University in New York. The two most obvious objects in the problem description are "student" and "course", so let's create classes for them. For ease of initialization, let's just make them subclasses of <init-object>. In fact, since a <person> is already a subclass of <init-object> and already has a name and an age, let's make <student> a subclass of <person>, with only the additional information and methods that aren't already in the <person> class.

(define <student> (make-class (list <person>)
                  (list 'credits 'course-list)))
(define <course> (make-class (list <init-object>)
                  (list 'name 'room 'time 'prof 'student-list)))

スロットの名前をユーザに知られたくないから、アクセス関数を定義しましょう。

Since we don't want users of these objects to know the slot names, we'll define some access functions:

(add-method get-name
     (make-method (list <student>)
           (lambda (cnm student) (slot-ref student 'name))))
(add-method get-courses
     (make-method (list <student>)
           (lambda (cnm student) (slot-ref student 'course-list))))
(add-method get-class
     (make-method (list <student>)
           (lambda (cnm student)
               (let ((credits (slot-ref student 'credits)))
                    (cond ((< credits 30) 'freshman)
                          ((< credits 60) 'sophomore)
                          ((< credits 90) 'junior)
                          (else 'senior))))))
(add-method get-name
     (make-method (list <course>)
           (lambda (cnm course) (slot-ref course 'name))))
(add-method get-room
     (make-method (list <course>)
           (lambda (cnm course) (slot-ref course 'room))))
(add-method get-time
     (make-method (list <course>)
           (lambda (cnm course) (slot-ref course 'time))))
(add-method get-prof-name
     (make-method (list <course>)
           (lambda (cnm course) (slot-ref course 'prof))))
(add-method get-roster
     (make-method (list <course>)
           (lambda (cnm course) (slot-ref course 'student-list))))

ほとんどのアクセス関数は単純にスロットの参照を呼ぶだけですが、そうである必要がないことに注意してください。もしユーザが同様に学生クラスを作ろうとしたとき、???

Notice that, although most of these access functions are simply calls to slot-ref, there's no reason they need be: if users are likely to want a student's class standing, yet the implementer decides to store the number of credits the student has completed, an "access function" like get-class may do significant work of its own.

Recall that we made <student> and <course> subclasses of <init-object> to allow easy initialization. However, let's insist that all students are created with no courses, and all courses are created with no students. We can do this by overriding their initialize methods:

(add-method initialize
    (make-method (list <student>)
        (lambda (call-next-method student initargs)
            (call-next-method)
            (slot-set! student 'course-list ()))))
(add-method initialize
    (make-method (list <course>)
        (lambda (call-next-method course initargs)
            (call-next-method)
            (slot-set! course 'student-list ()))))

In other words, even if some user does try to provide a course list or student list as an argument to make, they'll be set back to the empty list afterwards.

Of course, the main reason for keeping track of students and courses is for the former to take the latter. So we'd like to write two functions add and drop, each taking a student and a course: (add sam csc272) registers Sam for CSC 272, both adding Sam to the course roster for CSC 272 and adding CSC 272 to Sam's schedule, while (drop sam csc272) removes Sam from the course roster and CSC 272 from Sam's schedule.

Of course, things might go wrong. Sam might try to add a course for which he's already registered, or to drop a course for which he's not already registered, or to add a course that conflicts with another course he's already taking, etc. So we may need these functions to return some kind of error indication if they don't work. If they do work, let's have them return #f so the result can be tested easily, e.g. (if (add sam csc272) ...)

(add-method add
    (make-method (list <student> <course>)
        (lambda (cnm student course)
            (cond ((memv student (get-roster course))
                   "Error: student already in course roster")
                  ((memv course (get-courses student))
                   "Error: course already in student's list of courses")
                  (else (add-student student course)
                        (add-course course student)
                        #f)))))
(add-method drop
    (make-method (list <student> <course>)
        (lambda (cnm student course)
            (cond ((not (memv student (get-roster course)))
                   "Error: student not in course roster")
                  ((not (memv course (get-courses student)))
                   "Error: course not in student's list of courses")
                  (else (remove-student student course)
                        (remove-course course student)
                        #f)))))

These functions take care of the error-checking, but rely on four more (as yet unwritten) functions named add-student, add-course, remove-student, and remove-course to do the actual change. Since these four functions have to modify the internal state of <student> and <course> objects, we'll make them part of the interface to these classes:

(add-method add-student
    (make-method (list <student> <course>)
        (lambda (cnm student course)
            (slot-set! course 'student-list
                (cons student (get-roster course))))))
(add-method add-course
    (make-method (list <course> <student>)
        (lambda (cnm course student)
            (slot-set! student 'course-list
                (cons course (get-courses student))))))
(add-method remove-student
    (make-method (list <student> <course>)
        (lambda (cnm student course)
            (slot-set! course 'student-list
                (delv student (get-roster course))))))
(add-method remove-course
    (make-method (list <course> <student>)
        (lambda (cnm course student)
            (slot-set! student 'course-list
                (delv course (get-courses student))))))
Sasada Koichi / sasada@namikilab.tuat.ac.jp
$Date: 2003/02/26 13:07:29 $