Adaの日本語情報を増やす(7)・OOPその2

OOP続き。実は書いている人も勉強しながらメモまくってるだけなので、間違った情報があるかも。

前回の記事で派生型が副プログラムを継承することを説明しました。その知識を踏まえた上で、今回はAda85で追加されたtagged recordによる、本格的なOOPに入ります。

さて、前回の派生型では、record型の派生を例に副プログラムの継承(らしきもの)を解説しました。本来派生型はrecord型だけでなく、整数型や配列など、あらゆる型から派生することができるのですが、record型を使って解説を行ったのには理由があります。

OOPにおけるクラスとは大ざっぱに言えば一連のインスタンス変数(メンバ変数でもプロパティでも何でも良いですが)と、それを操作するメソッドの集合です。このうち、メンバ変数とはこれまた大ざっぱに言えば、変数名と値の対応関係を持った記憶域ということになります。Adaにおいてこのような対応関係を持つ型と言えば――そう、reocrd型です。Ada95で導入された本格的なOOPの仕組みも、このrecord型を拡張したtagged record型により実現されています。前回の記事でrecord型を使用したのは、今回の記事への土台だったわけですね。

前回の記事で解説したとおり、いくつかの問題点を除けば、Ada83でもrecord型とprocedurefunctionの組み合わせにより、OOPの基本的な操作は可能となっています。しかし、この組み合わせによるOOPには、サブクラスでのインスタンス変数――つまりはrecordの要素――の拡張ができないという大きな問題があります。前回の記事ではVolumeというインスタンス変数をスーパークラス・サブクラス共通で使用していましたが、仮にサブクラスであるCoffeesにのみ、Beansというインスタンス変数を追加したい場合、単純なrecord型で実現できません。単純なrecord型から派生しても、要素の追加はできませんからね。

この問題を解決するためにAda95で導入されたのがtagged record型です。tagged record型は派生時の宣言において、新しい要素の追加が可能になったrecord型です。

さて、ではコードを読んでみましょう。

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure OOP2 is
   --- ほぼは前回と同じ
   package Drinks is
      type Drink is tagged private; -- カプセル化のためにprivate
      procedure Add(O : in out Drink; V : in Positive);
      procedure Spilt(O : in out Drink; V : in Positive);
      function Get_Volume(O : in Drink) return Natural;

   private
      type Drink is tagged record
         Volume : Natural := 0;
      end record;
   end Drinks;

   package body Drinks is
      procedure Add(O : in out Drink; V : in Positive) is
      begin
         O.Volume := O.Volume + V;
      end Add;

      procedure Spilt(O : in out Drink; V : in Positive) is
      begin
         O.Volume := O.Volume - V;
      end Spilt;

      function Get_Volume(O : in Drink) return Natural is
      begin
         return O.Volume;
      end Get_Volume;
   end Drinks;
   
   
   -- Coofeesクラス
   package Coffees is
      type Coffee is new Drinks.Drink with private; -- やっぱりカプセル化
      -- 豆の種類に関する型とサブプログラム。Coffeesクラス専用
      type Beans_Type is (Blue_Mountain, Crystal_Brend, European_Brend);
      procedure Set_Beans_Type(O : in out Coffee; T : in Beans_Type);
      function Get_Beans_Type(O : in Coffee) return Beans_Type;

   private
      -- 拡張するインスタンス変数を定義
      type Coffee is new Drinks.Drink with record
         Beans : Beans_Type;
      end record;
   end Coffees;

   -- 実装
   package body Coffees is
      procedure Set_Beans_Type(O : in out Coffee; T : in Beans_Type) is
      begin
         O.Beans := T;
      end Set_Beans_Type;
      function Get_Beans_Type(O : in Coffee) return Beans_Type is
      begin
         return O.Beans;
      end Get_Beans_Type;
   end Coffees;

   Cup_C : Coffees.Coffee;

begin
   Coffees.Add(Cup_C, 200);
   Coffees.Set_Beans_Type(Cup_C, Coffees.Blue_Mountain);
   put("Cup_C: "); Put(Coffees.Get_Volume(Cup_C)); New_Line;
   Put("       "); Put(Coffees.Beans_Type'Image(Coffees.Get_Beans_Type(Cup_C))); New_Line;
end OOP2;

コード内の強調した部分を見れば分かるとおり、まず親クラス側では、

type Super_Record is tagged record ...;

taggedを付けて型の宣言を行います。この宣言により、派生型を宣言する際に要素の拡張が可能となります。

さて、前回から(tagged) record型をスペック内で宣言するときはprivateとして宣言してきました。インスタンス変数を隠蔽し、内部状態へはアクセサメソッドを経由してアクセスするという、カプセル化を意識した設計によるものです。コードを見れば分かるとおり、Adaにおけるインスタンスメソッドの可視性は全部見せるか全部隠すの二者択一となります。Javaの様に変数ごとの設定が出来ないため、やや不便ではありますが、一部だけpublicにすることは余りないので実害は余りないでしょう。なお、この場合の可視性がJavaでいうprivateになるかprotectedになるかはパッケージ構成によって異なります。詳しい話は後述しますが、上のコードのような親子関係のないパッケージ間ではprivateとして扱われます。つまり、サブクラスであるCoffeesパッケージ内からはスーパークラスであるDrinks.Drinkの要素については一切振れることが出来ません。

もう1つここでの注意点として、privatelimited privateの使い分けがあります。この2つの型の違いは主に代入の可否と演算子(特に比較演算子)の使用の可否にあります。コードをみてみましょう。

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Private_Test is
   package Test is
      type Foo is tagged limited private;
      type Bar is tagged private;
      procedure Set(O : out Foo; V : in Integer);
      function Get(O : in Foo) return Integer;
      procedure Set(O : out Bar; V : in Integer);
      function Get(O : in Bar) return Integer;

   private
      -- 定義自体は同じ
      type Foo is tagged limited record
         P : Integer;
      end record;
      type Bar is tagged record
         P : Integer;
      end record;
   end Test;

   package body Test is
      -- まったく同じメソッドを用意する
      procedure Set(O : out Foo; V : in Integer) is
      begin
         O.P := V;
      end Set;
      function Get(O : in Foo) return Integer is
      begin
         return O.P;
      end Get;
      procedure Set(O : out Bar; V : in Integer) is
      begin
         O.P := V;
      end Set;
      function Get(O : in Bar) return Integer is
      begin
         return O.P;
      end Get;
   end Test;

   X : Test.Foo;
   Y : Test.Foo;

   N : Test.Bar;
   M : Test.Bar;
begin
   Test.Set(X, 10);
   Test.Set(Y, 20);
-- X := Y;
-- if Test."="(X, Y) then
--    Put("X = Y"); New_Line;
-- else
--    Put("X /= Y"); New_Line;
-- end if;

   Test.Set(N, 10);
   Test.Set(M, 20);
   Put("N: "); Put(Test.Get(N)); New_Line;
   Put("M: "); Put(Test.Get(M)); New_Line;
   M := N;
   Put("N: "); Put(Test.Get(N)); New_Line;
   Put("M: "); Put(Test.Get(M)); New_Line;
   if Test."="(N, M) then
      Put("N = M"); New_Line;
   else
      Put("N /= M"); New_Line;
   end if;
end Private_Test;

private型であるMNがそれぞれ代入や比較が可能なのに対し、limited private型であるXYは代入や比較が出来ません(コメントを外すとコンパイルエラーになります)。

さて、この2つの違いはOOPにおいてどのような差を生むでしょうか。まずは、代入について考えてみましょう。

Adaにおいて、変数間での(tagged) recordの代入は要素の値のコピーを意味します。とても当たり前のような感じがしますが、Javaの代入が基本的には参照のコピーであることを思い出してください。Javaの代入ではインスタンスそのものは2つに増えません(名前が2つに増えただけです)。対してAdaの代入はまるでJavaのclone()メソッドを呼んだかのように、インスタンスそのものが2つに増えることになります。しかもこれはディープコピーです。

次に、比較について考えてみましょう。

これまたJavaとの比較になりますが、String型を比較するときに使用するequals()メソッドを思い出してください。これは該当インスタンスが同一でなくても、内容である文字列が同じならばtrueを返すメソッドです。もし、内容ではなく、違う変数名を持つ同一のインスタンスかどうかを調べたい場合は、単純に比較演算子==を使用して比較を行います。Adaの比較演算子はどちらのタイプでしょうか?

Adaおける比較演算子は――実はどちらでもありません。Adaにおいてはrecord型の比較は全ての要素が=ならばそのreocrd=である、という仕組みで動作します。これはつまりインスタンスそれ自身の同一性には無頓着であるということを意味しますが、だからといってJavaのequals()の様ほど中身を気にしてくれるかといえばそうでもありません。

たとえば何かの集合を配列で実装した場合、2つのインスタンスに含まれる何かが同じならば、配列中でどの位置に保存されていようと同じ集合であるとと見なしたいところです。このようなクラスの比較を行う場合、Adaの比較演算子は役に立ちません。配列の比較は同じ添字を持つ要素を順番に比較することで行われるので、順序が違うとその時点でfalseとなってしまうからです。こういった時、Javaならば==演算子の使用が適切でないことがよく知られているのでequals()メソッドが存在するかどうかを調べるのが自然です。しかしAdaの場合、Ada83の時代から比較演算子が普通に使われているという事情から、第三者が用意したクラスを使用するときなど、無意識のうちに単純な比較演算子で2つの集合を比較してしまうことが普通に起こり得ます

だんだんデスマス口調に疲れてきました。まぁとにかく、Adaの比較演算子は手続き型のプログラムを書くときには便利ですが、OOPで使用するといらぬ混乱を招きかねない、ということです。

このように、考え無しに増殖するインスタンスと意図しない比較による混乱を防止するために、初めのうちはlimited privateの使用をおすすめします。ただし、この後説明するaccess型を使用したよりJavaに近いプログラミングスタイルを使用する場合、private型で定義しておいた方が便利なこともあるため、一概には断定できないのが難しいところです。