10 Classes and Objects

Oz のクラスは次のものを含んでいます:

10.1第一原理からのクラス

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

Figure 10.1: クラス構築の例


あなたはメソッド init が属性 val を値 Value に割り当て、メソッド inc が属性 val を増加させ、メソッド browseval の現在の値をブラウズする事を見る事が出来るでしょう。

10.2第一原理からのオブジェクト

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

Figure 10.2: オブジェクトの構築


私達は以下の様にプログラムを試してみる事が出来ます

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}

10.3実際のオブジェクトとクラス

Oz は上で筋書きが書かれた方法論に従って、オブジェクト指向プログラミングをサポートします。構文サポートと最適化された実装もあり、オブジェクトの適用(オブジェクト中のメソッド呼び出しによる)も手続きの呼び出しと同じくらい安価です。先に定義されたクラス CounterFigure 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

Figure 10.3: Counter クラス


クラス X は次によって定義されます:

class X ... end

属性はメソッド宣言部の前の属性宣言部を使って定義されます:

attr A1 ... AN

そしてメソッド定義が続きます、それらは次の形式を持っています:

meth E S 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

10.3.1静的メソッド(static method)の呼び出し

クラス C があってメソッド頭部 m(...) が以下の形式を持つメソッド呼び出しだとします:

C, m(...)

メソッド呼び出しはクラスの引数で定義されたメソッドを呼び出します。メソッド呼び出しはメソッド定義の中でのみ使う事が出来ます。これがメソッド呼び出しが self で示される現在のオブジェクトを暗黙の引数として取る理由です。メソッドはクラス C かまたはその親クラスから継承してきたもので定義する事が出来ます。継承は手短に説明されます。

10.3.2モジュールとしてのクラス

静的メソッドの呼び出しは一般的に手続き呼び出しと同じ効率を持っています。これはクラスにモジュールの仕様として使われる事を許します。これはクラスが継承によって段階的に構築可能である事による優位性かもしれません。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

Figure 10.4: List クラス


モジュール仕様からモジュールを生成するのは、クラスからオブジェクトを生成する事を必要とします。これは次によって行われます:

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]  $)}}

10.4継承(inheritance)

クラスは次のキーワードの後に見える1つまたはいくつかのクラスを継承するかもしれません: fromクラス BA親クラス(superclass) です、もし:

継承は既存のクラスから新しいクラスを構築する手段です。それはどんな属性、フィールド名3、メソッドが新しいクラスで利用できるのかを定義します。継承の話をメソッドに限定しましょう。なお、同じルールがフィールド名と属性にも適用できます。

クラス C で利用可能なメソッド(つまり可視なもの)はクラス階層で見られるメソッドに優先的に関係しています。私達はこの関係をオーバーライド関係(overriding relation)と呼びます:

親クラス関係のクラス階層はルートで定義されたクラスによって有向グラフに見えます。辺はサブクラスを指しています。継承が適正であるために2つの要求があります。最初は、継承関係は方向付けられていて非循環という事です。それゆえ、以下は許可されません:

class A from B ... end 
class B from A ... end


Figure 10.5: 不正なクラス階層


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


Figure 10.6: メソッド m で不正なクラス


下のクラス C も2つのメソッド mC 中で利用可能なので不正です。

class A meth m(...... end end 
class B meth m(...... end end 
class C from A B end

あなたが不正な階層を持つプログラムを走らせると、システムは不正なメソッドにアクセスしようとしたオブジェクトが作られるまで文句を言わないでしょう。この時点になって、あなたは実行時エラーを得るでしょう。その理由は、クラスはコンパイル時に個別に形作られ、実行時にメソッドキャッシュを使って要求によって完成するからです。

10.4.1多重継承(multiple inheritance)か否か

私の意見は以下です:

10.5フィールド名(feature)

オブジェクトはレコードに似たフィールド名を持っているかもしれません。フィールド名はクラス宣言時に指定されるステートレスなコンポーネントです:

class C from ... 
   feat 
A1 ... 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

10.5.1フィールドの初期化

例はクラス定義時にどの様にフィールドが初期化されるかを示します。このケースでは、クラス 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 の同じ値に束縛するのが見えるでしょう。

フィールド名について言った事は属性にも当てはまります。

10.6パラメータ化されたクラス

あなたのクラスをより汎用的(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

Figure 10.7: パラメータ化されたクラス


私達は今、整数と有理数のために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*< 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)] $)}}

10.7自己適用(self application)

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 を取り上げる事を意味します。その後、私達はオブジェクトを(手続きとして)メッセージに適用します。これは全てのオブジェクト指向言語に共通する動的束縛の形式です。

10.8属性(attribute)

私達は属性の記法に以前触れました。属性はオブジェクトで状態を持ち越します。属性はフィールド名の様に宣言されますが、キーワード attr を代わりに使います。オブジェクトが生成される時、各属性は新しいセルがその値として割り当てられます。これらのセルはフィールドと同じ方法で初期化されます。実際の違いは属性がセルで、割り当て、再度割り当てと任意のアクセスが任意に可能であるという事にあります。しかしながら、属性はオブジェクトにプライベートなものです。外側から属性を操作する唯一の方法は、クラスの設計者に属性を操作するメソッドを書いてもらう事です。Figure 10.8 で私達はクラス Point を定義します。属性 xy は初期化メッセージが適用される前に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

Figure 10.8: クラス Point


Point のインスタンス生成といくつかのメッセージの適用の試行:

declare P
P = {New Point init(2 0)}
{P display}
{P move(3 2)}

10.9プライベート(private)とプロテクテッド(protected)なメソッド

メソッドはリテラルの代わりに変数によってラベル付けされるかもしれません。これらのメソッドは次の様に、定義されたクラスでプライベート(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

Figure 10.9: クラス History Point


HistoryPoint のクラス定義にはいくつも言及すべき所があります。最初はメソッドを洗練(refine)させる典型的パターンが見られる事です。メソッド move はクラス Point のものを特化させたものです。それは最初に親クラスのメソッドを呼び出し、その後 HistoryPoint クラス特有のものを行います。2番目に、DisplayHistory メソッドはクラスにプライベートなものになっている事です。さらにはそれはサブクラスでも利用可能になっています、つまりプロテクテッドです、これは属性 displayHistory に保存する事で行われます。あなたはクラスを以下の文の様に試す事が出来ます:

declare P
P = {New HistoryPoint init(2 0)}
{P display}
{P move(3 2)}

10.10デフォルトの引数値

メソッド頭部はデフォルトの引数値を持つかもしれません。以下の例を考えて見て下さい。

meth m(X Y d1:Z<=0 d2:W<=0) ... end

メソッド m の呼び出しはフィールド名 d1d2 を未指定のままにしておくかもしれません。この場合、これらの引数は値0とみなされます。

Point の例を Point を他の方向で特化させて続けます。クラス BoundedPoint を制約された長方形領域で移動する点として定義します。領域外の点への移動の試みは無視されるでしょう。このクラスは Figure 10.10 に示されます。メソッド initBoundedPoint のインスタンス初期化時に指定されていなければデフォルトの領域を与える 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.andthen 
       X =< @xbounds.andthen 
       Y >= @ybounds.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

Figure 10.10: クラス BoundedPoint


私達はこのセクションを多重継承問題を示す事によって私達の例を終わらせる事により締めます。私達は今 HistoryPointBoundedPoint の両方を bounded-history point として特化させたいと思っています。点は履歴を記録し続け、制約された領域を移動します。私達はこれを2つの前に定義したクラスを継承するクラス BHPoint を定義する事により行います。それらはステートフルな属性を含んだクラス Point を共有しているので、私達は実装共有問題に遭遇します。私達は色々この問題を考え、同じアクションを繰り返す事を避けるために boundConstraintdisplayHistory に保存された2つのプロテクテッドメソッドを作成しました。どの場合でも、私達はメソッド init, move, display を洗練させなくてはなりません、なぜならそれらは2つの兄弟クラスで発生するからです。解法は Figure 10.11 で示されます。私達がプロテクテッドメソッドを使っている事に注意して下さい。私達は属性 xy の初期化の重複を避ける事を気にしません、なぜならそれは何の悪影響も持たないからです。以下の例を試してみて下さい:

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

Figure 10.11: クラス BHPoint



1. 実際には、クラスはいくつかの不可視な状態を持つかもしれません。現在の実装では、クラスは通常ステートフルなメソッドキャッシュを持ちます。
2. これは簡易化です; Oz のオブジェクトはそのフィールドの一つで上の手続きを持つチャンクです; その他のフィールドはオブジェクトのフィールド名を持ちます
3. 短く定義するため

Seif Haridi and Nils Franz�n
Version 1.4.0 (20080704)