<< Prev | - Up - | Next >> |
Oz のクラスは次のものを含んでいます:
メソッドテーブルのメソッドのコレクション。
クラスの各インスタンスが所有する属性(attribute)の記述。各属性は属性の名前によってアクセス出来るステートフルなセルで、アトムか Oz-name のどちらかです。
クラスの各インスタンスが所有するフィールド名の記述。フィールドはフィールド名でアクセス出来る代入不可のコンポーネント(変数)で、アトムか Oz-name のどちらかです。
クラスはステートレスな Oz の値です1。Smalltalk, Java その他の言語と対照的に、それらはクラスのオブジェクトがどの様に振る舞うべきかを記述したものにすぎません。
Figure 10.1 は上の筋書きに沿った第一原理からどの様にクラスが構築されるかを示しています。ここで私達は Counter
クラスを構築します。それはアトム val によってアクセスされる単一の属性を持ちます。それはメソッドテーブルを持ち、それはチャンクのフィールド名を通してアクセスされる3つのメソッド browse
, init
, inc
を持ちます。メソッドは、常にレコードである、現在のオブジェクトの状態を表し内部的に self
として知られている追加のパラメータであるメッセージを取る、手続きです。
declare Counter
local
Attrs = [val]
MethodTable = m(browse:MyBrowse init:Init inc:Inc)
proc {Init M S Self}
init(Value) = M in
(S.val) := Value
end
proc {Inc M S Self}
X inc(Value)=M
in
X = @(S.val) (S.val) := X+Value
end
proc {MyBrowse M=browse S Self}
{Browse @(S.val)}
end
in
Counter = {NewChunk c(methods:MethodTable attrs:Attrs)}
end
あなたはメソッド init
が属性 val
を値 Value
に割り当て、メソッド inc
が属性 val
を増加させ、メソッド browse
が val
の現在の値をブラウズする事を見る事が出来るでしょう。
Figure 10.2 は与えられたクラスからオブジェクトを生成するジェネリックな手続きを示します。この手続きはクラスの属性からオブジェクトを生成します。これはオブジェクトの属性を、それぞれセルとして(初期値は未束縛なものとして)初期化します。ここで私達は、レコードの全フィールドを反復する反復子 Record.forAll/2
を使います。NewObject
はオブジェクトを特定する手続き Object
を返します。オブジェクトの状態は Object
の中でのみ可視である事に注視して下さい。ある人は Object
は状態をカプセル化した手続きだと言うかもしれません 2。(訳注:これはLisp族での環境を伴った関数定義を想定しているのでしょう)
proc {NewObject Class InitialMethod ?Object}
State O
in
State = {MakeRecord s Class.attrs}
{Record.forAll State proc {$ A} {NewCell _ A} end}
proc {O M}
{Class.methods.{Label M} M State O}
end
{O InitialMethod}
Object = O
end
私達は以下の様にプログラムを試してみる事が出来ます
declare C
{NewObject Counter init(0) C}
{C inc(6)} {C inc(6)}
{C browse}
以下の文を実行してみて下さい。
local X in {C inc(X)} X=5 end {C browse}
何も起こらない事が見て取れるでしょう。その理由はオブジェクトの適用
{C inc(X)}
がメソッド inc
を実装している手続き Inc/3
の内部で一時停止するからです。それがどこだか正確に分かりますか?他方、以下の文の実行では、期待される様に働きます。
local X in thread {C inc(X)} end X=5 end {C browse}
Oz は上で筋書きが書かれた方法論に従って、オブジェクト指向プログラミングをサポートします。構文サポートと最適化された実装もあり、オブジェクトの適用(オブジェクト中のメソッド呼び出しによる)も手続きの呼び出しと同じくらい安価です。先に定義されたクラス Counter
は Figure 10.3 で示される様な構文形式を持っています:
class Counter
attr val
meth browse
{Browse @val}
end
meth inc(Value)
val := @val + Value
end
meth init(Value)
val := Value
end
end
クラス X は次によって定義されます:
class
X... end
属性はメソッド宣言部の前の属性宣言部を使って定義されます:
attr
A1...
AN
そしてメソッド定義が続きます、それらは次の形式を持っています:
meth
ES
end
式 E がメソッドの頭部として評価される時、それはそのラベルがメソッド名であるレコードです。属性 A は式 @
A を使ってアクセスされます。値の割り当ては文 A :=
E を使って行われます。
クラスは次の様に匿名的に定義する事も出来ます:
X = class $ ... end
以下は、オブジェクトがクラスから手続き New/3
を使ってどの様に生成されるかを示しています、その最初の引数はクラスで、2番目は初期化メソッドで結果はオブジェクトです。New/3
はクラスからオブジェクトを生成するためのジェネリックな手続きです。
declare C in
C = {New Counter init(0)}
{C browse}
{C inc(1)}
local X in thread {C inc(X)} end X=5 end
クラス C があってメソッド頭部 m(...)
が以下の形式を持つメソッド呼び出しだとします:
C,
m(...)
メソッド呼び出しはクラスの引数で定義されたメソッドを呼び出します。メソッド呼び出しはメソッド定義の中でのみ使う事が出来ます。これがメソッド呼び出しが self
で示される現在のオブジェクトを暗黙の引数として取る理由です。メソッドはクラス C
かまたはその親クラスから継承してきたもので定義する事が出来ます。継承は手短に説明されます。
静的メソッドの呼び出しは一般的に手続き呼び出しと同じ効率を持っています。これはクラスにモジュールの仕様として使われる事を許します。これはクラスが継承によって段階的に構築可能である事による優位性かもしれません。Figure 10.4 に示されるプログラムはクラスがモジュールの仕様として振る舞える事を示しています。クラス ListC
はメソッドとしていくつかの共通のリスト手続きを定義しています。ListC
はメソッド append/3
, member/2
, length/2
, nrev/2
を定義しています。メソッド本体部が Oz 文と似ていて、しかしメソッド呼び出しも許されている事を注視して下さい。私達はここで継承の最初の例も見ます。
class ListC from BaseObject
私達は関数的メソッド、すなわち関数に似た結果を返すメソッドもお見せします。関数的メソッドは一般に以下の様な形式を持っています:
meth m( ... $) S E end
ここでクラス ListC
は事前に定義されたクラス BaseObject
(自明なメソッド meth noop() skip end
を持つ)を継承しています。
class ListC from BaseObject
meth append(Xs Ys $)
case Xs
of nil then Ys
[] X|Xr then
X|(ListC , append(Xr Ys $))
end
end
meth member(X L $)
{Member X L} % これは List.oz で定義されています
end
meth length(Xs $)
case Xs
of nil then 0
[] _|Xr then
(ListC , length(Xr $)) + 1
end
end
meth nrev(Xs ?Ys)
case Xs
of nil then Ys = nil
[] X|Xr then Yr in
ListC , nrev(Xr Yr)
ListC , append(Yr [X] Ys)
end
end
end
モジュール仕様からモジュールを生成するのは、クラスからオブジェクトを生成する事を必要とします。これは次によって行われます:
declare ListM = {New ListC noop}
ListM
はモジュールとしての役割を果たすオブジェクトです、すなわち、それは手続き(メソッド)の群をカプセル化します。私達はこのモジュールをいくつかのメソッド呼び出しを行う事によって試す事が出来ます:
{Browse {ListM append([1 2 3] [4 5] $)}}
{Browse {ListM length([1 2 3] $)}}
{Browse {ListM nrev([1 2 3] $)}}
クラスは次のキーワードの後に見える1つまたはいくつかのクラスを継承するかもしれません: from
クラス B は A の 親クラス(superclass) です、もし:
B が A の宣言で from
中に表れるならば、または
B が A の宣言で from
中に表れるクラスの親クラスであるならば。
継承は既存のクラスから新しいクラスを構築する手段です。それはどんな属性、フィールド名3、メソッドが新しいクラスで利用できるのかを定義します。継承の話をメソッドに限定しましょう。なお、同じルールがフィールド名と属性にも適用できます。
クラス C で利用可能なメソッド(つまり可視なもの)はクラス階層で見られるメソッドに優先的に関係しています。私達はこの関係をオーバーライド関係(overriding relation)と呼びます:
クラス C のメソッドは親クラス C の同じラベルのどのメソッドもオーバーライドします。
親クラス関係のクラス階層はルートで定義されたクラスによって有向グラフに見えます。辺はサブクラスを指しています。継承が適正であるために2つの要求があります。最初は、継承関係は方向付けられていて非循環という事です。それゆえ、以下は許可されません:
class A from B ... end
class B from A ... end
2番目は、全てのオーバーライドされたメソッドを退去させた後、階層中のクラスの中において残ったメソッドはユニークなラベルを持っていてかつ定義されているという事です。それゆえ、以下の例のクラス C
は2つのラベル m
が残っているので不正です。
class A1 meth m(...) ... end end
class B1 meth m(...) ... end end
class B from B1 end
class A from A1 end
class C from A B end
下のクラス C
も2つのメソッド m
が C
中で利用可能なので不正です。
class A meth m(...) ... end end
class B meth m(...) ... end end
class C from A B end
あなたが不正な階層を持つプログラムを走らせると、システムは不正なメソッドにアクセスしようとしたオブジェクトが作られるまで文句を言わないでしょう。この時点になって、あなたは実行時エラーを得るでしょう。その理由は、クラスはコンパイル時に個別に形作られ、実行時にメソッドキャッシュを使って要求によって完成するからです。
私の意見は以下です:
一般的に、多重継承の使用を適正に行うには、使用者は全ての継承階層を理解し、それは時たま努力に見合う価値がある。これは共通の祖先がある時に重要です。
Oz は多重継承をそれにまつわる問題の多くが起こらないように制限しています。
Oz は使用者に1つより多くの親クラスで定義されているメソッドのオーバーライドを要求し、ローカルで競合するメソッドの内でどれをオーバーロードするか定義しなければならないという、プログラミングの方法論を強制します。
多重継承には兄弟(sibling)親クラスが直接的間接的に共通のステートフル(つまり属性を持つ)な祖先クラスを共有している時にも問題があります。ある者は同じ属性に対する操作の複製を得るかもしれません。これはクラスにおいて初期化メソッドの実行時に典型的に起きるもので、親クラスの初期化をしなくてはなりません。ここでの唯一の対処法は注意深く継承階層を理解してその様な複製を避ける事です。別の方法は、ステートフルな共通の祖先を共有していない複数のクラスのみを継承する事です。このプログラムは実装共有問題として知られています。
オブジェクトはレコードに似たフィールド名を持っているかもしれません。フィールド名はクラス宣言時に指定されるステートレスなコンポーネントです:
class
Cfrom ...
A1
feat...
AN
...
end
レコードの様に、オブジェクトのフィールド名は対応するフィールドを持っています。フィールドは Oz の値(セル、オブジェクト、クラスetc)に束縛されうる論理変数です。オブジェクトのフィールド名は中置演算子 '.
' を使ってアクセスされます。以下はフィールド名の使用例です:
class ApartmentC from BaseObject
meth init skip end
end
class AptC from ApartmentC
feat
streetName: york
streetNumber:100
wallColor:white
floorSurface:wood
end
例はクラス定義時にどの様にフィールドが初期化されるかを示します。このケースでは、クラス AptC
の全インスタンスがクラスのフィールド名を対応する値とともに持つでしょう。それゆえ、以下のプログラムは york
を2回表示するでしょう。
declare Apt1 Apt2
Apt1 = {New AptC init}
Apt2 = {New AptC init}
{Browse Apt1.streetName}
{Browse Apt2.streetName}
私達は次の様にフィールドを初期化しないまま去る事も出来ます:
class MyAptC1 from ApartmentC
feat streetName
end
この場合、どんなインスタンスが生成されても、フィールド名のフィールドは新しい未使用の論理変数が割り当てられます。それゆえ、以下のプログラムはオブジェクト Apt3
のフィールド名 streetName
をアトム kungsgatan
に束縛し、対応するフィールド名 Apt4
をアトム sturegatan
に束縛します。
declare Apt3 Apt4
Apt3 = {New MyAptC1 init}
Apt4 = {New MyAptC1 init}
Apt3.streetName = kungsgatan
Apt4.streetName = sturegatan
1つより多くの初期化が利用可能です。クラス定義のフィールドは変数または変数を持つ Oz の値に初期化出来ます。以下では、フィールドは匿名変数のタプルに初期化されます。この場合、クラスの全てのインスタンスは同じ変数を共有(share)します。以下のプログラムについて考えましょう。
class MyAptC1 from ApartmentC
feat streetName:f(_)
end
local Apt1 Apt2 in
Apt1 = {New MyAptC1 init}
Apt2 = {New MyAptC1 init}
{Browse Apt1.streetName}
{Browse Apt2.streetName}
Apt1.streetName = f(york)
段階的に入力すると、文
Apt1.streetName = f(york)
が対応する Apt2
のフィールド名を Apt1
の同じ値に束縛するのが見えるでしょう。
フィールド名について言った事は属性にも当てはまります。
あなたのクラスをより汎用的(generic)にする多くの手段があります、それは後で特定の目的に特化(specialize)する事が出来ます。オブジェクト指向プログラミングでこれを行う共通の方法はまず最初にいくつかのメソッドが未指定で残されている抽象クラス(abstract class)を定義する事です。後でこれらのメソッドはサブクラスで定義されます。比較演算子 less
を必要とするソートのための汎用クラスを定義したと想定して下さい。この演算子はどの種のデータがソートされるのかに依存します。整数、有理数、複素数etcのために異なる具象化が必要とされます。この場合、サブクラス化する事によって抽象クラスを実体(concrete) クラスに特化出来ます。
Oz では、私達はジェネリッククラスを生成するための他の自然なメソッドも持っています。クラスは第一級の値(first-class value)であるので、私達は代わりにいくつかの型引数を取って型に特化されたクラスを返す関数を定義する事が出来ます。Figure 10.7 では、クラスを単一の引数として取って引数のために特化したソートクラスを返す関数 SortClass
が定義されています。
fun {SortClass Type}
class $ from BaseObject
meth qsort(Xs Ys)
case Xs
of nil then Ys = nil
[] P|Xr then S L in
{self partition(Xr P S L)}
ListC, append({self qsort(S $)} P|{self qsort(L $)} Ys)
end
end
meth partition(Xs P Ss Ls)
case Xs
of nil then Ss = nil Ls = nil
[] X|Xr then Sr Lr in
case Type,less(X P $) then
Ss = X|Sr Lr = Ls
else
Ss = Sr Ls = X|Lr
end
{self partition(Xr P Sr Lr)}
end
end
end
end
私達は今、整数と有理数のために2つのクラスを定義出来ます:
class Int
meth less(X Y $)
X<Y
end
end
class Rat from Object
meth less(X Y $)
'/'(P Q) = X
'/'(R S) = Y
in
P*S < Q*R
end
end
それゆえ、私達は以下の文を実行出来ます:
{Browse {{New {SortClass Int} noop} qsort([1 2 5 3 4] $)}}
{Browse {{New {SortClass Rat} noop}
qsort(['/'(23 3) '/'(34 11) '/'(47 17)] $)}}
Figure 10.7 のプログラムはメソッド qsort
でオブジェクトの適用がキーワード self
を使って行われているのを示しています(下を見て下さい)。
meth qsort(Xs Ys)
case Xs
...
{self partition(Xr P S L)}
...
end
ここで私達は用語オブジェクト適用(object-application)を一般的に知られる用語メッセージ送信(message sending)の代わりに使っています、なぜならメッセージ送信は Oz の様な並行言語ではミスリーディングだからです。私達が self
をオブジェクト指定の代わりに次の様に使う時
{self partition(Xr P S L)}
私達は動的に現在のオブジェクトで定義されている(利用可能な)メソッド partition
を取り上げる事を意味します。その後、私達はオブジェクトを(手続きとして)メッセージに適用します。これは全てのオブジェクト指向言語に共通する動的束縛の形式です。
私達は属性の記法に以前触れました。属性はオブジェクトで状態を持ち越します。属性はフィールド名の様に宣言されますが、キーワード attr
を代わりに使います。オブジェクトが生成される時、各属性は新しいセルがその値として割り当てられます。これらのセルはフィールドと同じ方法で初期化されます。実際の違いは属性がセルで、割り当て、再度割り当てと任意のアクセスが任意に可能であるという事にあります。しかしながら、属性はオブジェクトにプライベートなものです。外側から属性を操作する唯一の方法は、クラスの設計者に属性を操作するメソッドを書いてもらう事です。Figure 10.8 で私達はクラス Point
を定義します。属性 x
と y
は初期化メッセージが適用される前に0に初期化される事に注意して下さい。メソッド move
は 自己適用(self-application)
を内部で行っています。
class Point from BaseObject
attr x:0 y:0
meth init(X Y)
x := X
y := Y % 属性の更新
end
meth location(L)
L = l(x:@x y:@y) % 属性にアクセス
end
meth moveHorizontal(X)
x := X
end
meth moveVertical(Y)
y := Y
end
meth move(X Y)
{self moveHorizontal(X)}
{self moveVertical(Y)}
end
meth display
% browser を仮想文字列モードに変更
{Browse "point at ("#@x#" , "#@y#")\n"}
end
end
Point
のインスタンス生成といくつかのメッセージの適用の試行:
declare P
P = {New Point init(2 0)}
{P display}
{P move(3 2)}
メソッドはリテラルの代わりに変数によってラベル付けされるかもしれません。これらのメソッドは次の様に、定義されたクラスでプライベート(private)です:
class C from ...
meth A(X) ... end
meth a(...) {self A(5)} ... end
....
end
メソッド A
はクラス C
の中でのみ可視です。実際には上の記法は以下の展開された定義の略記法です:
local A = {NewName} in
class C from ...
meth !A(X) ... end
meth a(...) {self A(5)} ... end
...
end
end
A はクラス定義のレキシカルスコープで新しい名前に束縛されます。
いくつかのオブジェクト指向言語はプロテクテッドメソッドの記法も持っています。メソッドが定義されたクラスまたはその子孫クラス、すなわちサブクラス、サブサブクラスetcでのみアクセス可能な時、メソッドはプロテクテッド(protected)です。Oz ではプロテクテッドなメソッドを定義する直接の方法はありません。しかしながら同じ効果を与えるプログラミングテクニックがあります。私達は属性が定義されたクラスまたはその継承による子孫クラスでのみ可視である事を知っています。私達はメソッドを最初にプライベートにして次にそれを属性にストアする事でプロテクテッドにする事が出来ます。以下の例を考えましょう:
class C from ...
attr pa:A
meth A(X) ... end
meth a(...) {self A(5)} ... end
...
end
今、私達は C
のサブクラス C1
と生成し、メソッド A
に以下の様にアクセスします:
class C1 from C
meth b(...) L=@pa in {self L(5)} ... end
...
end
メソッド b
はメソッド A
に属性 pa
を通してアクセスします。
前の移動の履歴を保存する点を加えるように特化されたクラスの定義によって Figure 10.8 の簡単な例で続けましょう。これは Figure 10.9 で見られます。
class HistoryPoint from Point
attr
history: nil
displayHistory: DisplayHistory
meth init(X Y)
Point,init(X Y) % 親クラスを呼び出します
history := [l(X Y)]
end
meth move(X Y)
Point,move(X Y)
history := l(X Y)|@history
end
meth display
Point,display
{self DisplayHistory}
end
meth DisplayHistory % プロテクテッドメソッドを作ります
{Browse "with location history: "}
{Browse @history}
end
end
HistoryPoint
のクラス定義にはいくつも言及すべき所があります。最初はメソッドを洗練(refine)させる典型的パターンが見られる事です。メソッド move
はクラス Point
のものを特化させたものです。それは最初に親クラスのメソッドを呼び出し、その後 HistoryPoint
クラス特有のものを行います。2番目に、DisplayHistory
メソッドはクラスにプライベートなものになっている事です。さらにはそれはサブクラスでも利用可能になっています、つまりプロテクテッドです、これは属性 displayHistory
に保存する事で行われます。あなたはクラスを以下の文の様に試す事が出来ます:
declare P
P = {New HistoryPoint init(2 0)}
{P display}
{P move(3 2)}
メソッド頭部はデフォルトの引数値を持つかもしれません。以下の例を考えて見て下さい。
meth m(X Y d1:Z<=0 d2:W<=0) ... end
メソッド m
の呼び出しはフィールド名 d1
と d2
を未指定のままにしておくかもしれません。この場合、これらの引数は値0とみなされます。
Point
の例を Point
を他の方向で特化させて続けます。クラス BoundedPoint
を制約された長方形領域で移動する点として定義します。領域外の点への移動の試みは無視されるでしょう。このクラスは Figure 10.10 に示されます。メソッド init
は BoundedPoint
のインスタンス初期化時に指定されていなければデフォルトの領域を与える 2つのデフォルト引数を持っています。
class BoundedPoint from Point
attr
xbounds: 0#0
ybounds: 0#0
boundConstraint: BoundConstraint
meth init(X Y xbounds:XB <= 0#10 ybounds:YB <= 0#10)
Point,init(X Y) % 親クラスの呼び出し
xbounds := XB
ybounds := YB
end
meth move(X Y)
if {self BoundConstraint(X Y $)} then
Point,move(X Y)
end
end
meth BoundConstraint(X Y $)
(X >= @xbounds.1 andthen
X =< @xbounds.2 andthen
Y >= @ybounds.1 andthen
Y =< @ybounds.2 )
end
meth display
Point,display
{self DisplayBounds}
end
meth DisplayBounds
X0#X1 = @xbounds
Y0#Y1 = @ybounds
S = "xbounds=("#X0#","#X1#"),ybounds=("
#Y0#","#Y1#")"
in
{Browse S}
end
end
私達はこのセクションを多重継承問題を示す事によって私達の例を終わらせる事により締めます。私達は今 HistoryPoint
と BoundedPoint
の両方を bounded-history point として特化させたいと思っています。点は履歴を記録し続け、制約された領域を移動します。私達はこれを2つの前に定義したクラスを継承するクラス BHPoint
を定義する事により行います。それらはステートフルな属性を含んだクラス Point
を共有しているので、私達は実装共有問題に遭遇します。私達は色々この問題を考え、同じアクションを繰り返す事を避けるために boundConstraint
と displayHistory
に保存された2つのプロテクテッドメソッドを作成しました。どの場合でも、私達はメソッド init
, move
, display
を洗練させなくてはなりません、なぜならそれらは2つの兄弟クラスで発生するからです。解法は Figure 10.11 で示されます。私達がプロテクテッドメソッドを使っている事に注意して下さい。私達は属性 x
と y
の初期化の重複を避ける事を気にしません、なぜならそれは何の悪影響も持たないからです。以下の例を試してみて下さい:
declare P
P = {New BHPoint init(2 0)}
{P display}
{P move(1 2)}
これはオブジェクトシステムのほとんどをよくカバーしています。残っているのはオブジェクトの共通領域を並行スレッドで共有する事の扱いをどうするかです。
class BHPoint from HistoryPoint BoundedPoint
meth init(X Y xbounds:XB <= 0#10 ybounds:YB <= 0#10)
% init 繰り返し
HistoryPoint,init(X Y)
BoundedPoint,init(X Y xbounds:XB ybounds:YB)
end
meth move(X Y)
L = @boundConstraint in
if {self L(X Y $)} then
HistoryPoint,move(X Y)
end
end
meth display
BoundedPoint,display
{self @displayHistory}
end
end
<< Prev | - Up - | Next >> |