OOP続き。実は書いている人も勉強しながらメモまくってるだけなので、間違った情報があるかも。
前回の記事で派生型が副プログラムを継承することを説明しました。その知識を踏まえた上で、今回はAda85で追加されたtagged recordによる、本格的なOOPに入ります。
さて、前回の派生型では、record型の派生を例に副プログラムの継承(らしきもの)を解説しました。本来派生型はrecord型だけでなく、整数型や配列など、あらゆる型から派生することができるのですが、record型を使って解説を行ったのには理由があります。
OOPにおけるクラスとは大ざっぱに言えば一連のインスタンス変数(メンバ変数でもプロパティでも何でも良いですが)と、それを操作するメソッドの集合です。このうち、メンバ変数とはこれまた大ざっぱに言えば、変数名と値の対応関係を持った記憶域ということになります。Adaにおいてこのような対応関係を持つ型と言えば――そう、reocrd型です。Ada95で導入された本格的なOOPの仕組みも、このrecord型を拡張したtagged record型により実現されています。前回の記事でrecord型を使用したのは、今回の記事への土台だったわけですね。
前回の記事で解説したとおり、いくつかの問題点を除けば、Ada83でもrecord型とprocedure・functionの組み合わせにより、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つここでの注意点として、privateとlimited 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型であるMとNがそれぞれ代入や比較が可能なのに対し、limited private型であるXとYは代入や比較が出来ません(コメントを外すとコンパイルエラーになります)。
さて、この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型で定義しておいた方が便利なこともあるため、一概には断定できないのが難しいところです。
続き。ほんとは「継承」って用語は正しくないんだよね。
今回はClass Wide型について解説する。その前に、メソッドの継承について若干確認を行う。まずはコードを。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure OOP3 is
package Foo_Package is
type Foo is record -- 中身は適当
Val_X : Integer;
end record;
procedure In_Procedure(X : Foo);
end Foo_Package;
package body Foo_Package is
procedure In_Procedure(X : Foo) is
begin
Put("In_Procedure called."); New_Line;
end In_Procedure;
end Foo_Package;
use Foo_Package;
-- パッケージ外にあるメソッド
procedure Out_Procedure(X : Foo) is
begin
Put("Out_Procedure called."); New_Line;
end Out_Procedure;
-- 継承
type Bar is new Foo;
X : Bar;
begin
In_Procedure(X);
-- Out_Procedure(X); -- パッケージの外なので継承されてない
end OOP3;
Adaにおける「クラス」はパッケージに相当する、という解説がよく行われる。実際、前回までの記事でも「Classes.Class」という命名規則を使用し、インスタンス変数のホルダーであるrecordと、メソッドとなる副プログラムをパッケージで区切って定義してきた。命名規則だけを見るとわざわざパッケージを定義するのは冗長に見えるが、これは派生型の性質上避けることが出来ない。以前解説したとおり、派生型が継承する副プログラムは、その派生型が定義されているパッケージ内で同時に定義された物に限られる。このため、副プログラム(メソッド)の継承を行いたければ、型(クラス)ごとにパッケージを分割する必要があるのだ。逆にパッケージを分割しない場合、副プログラムの継承は行われた無いため、コメントアウトされた行の様な呼び出しを行うとコンパイルエラーとなる。これらの理由から、Adaにおけるクラスの定義は基本的にはパッケージを分割することで行われる。
ところで、このようにして継承を行った場合、メソッドのオーバーライドには特に制限がない。しかし、状況によっては安全のため、サブクラスによるオーバーライドを防止したい場合もあるだろう。そのような際に便利なのがClass Wide型だ。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure OOP4 is
package Super_Classes is
type Super_Class is tagged record -- 面倒なのでprivateにしない
Val_X : Integer; -- 中身は適当
end record;
procedure Derived_Procedure(O : Super_Class);
procedure CW_Procedure(O : Super_Class'Class); -- これがClass Wide型
end Super_Classes;
package body Super_Classes is
procedure Derived_Procedure(O : Super_Class) is
begin
Put("Derived_Procedure called."); New_Line;
end Derived_Procedure;
procedure CW_Procedure(O : Super_Class'Class) is
begin
Put("CW_Procedure called."); New_Line;
end CW_Procedure;
end Super_Classes;
package Sub_Classes is
type Sub_Class is new Super_Classes.Super_Class with null record;
procedure Derived_Procedure(O : Sub_Class); -- オーバーライド
end Sub_Classes;
package body Sub_Classes is
procedure Derived_Procedure(O : Sub_Class) is
begin
Put("Overrided Derived_Procedure called."); New_Line;
end Derived_Procedure;
end Sub_Classes;
Super : Super_Classes.Super_Class;
Sub : Sub_Classes.Sub_Class;
begin
Super_Classes.Derived_Procedure(Super);
Super_Classes.CW_Procedure(Super);
Sub_Classes.Derived_Procedure(Sub);
Super_Classes.CW_Procedure(Sub);
-- Sub_Classes.CW_Procedure(Sub); -- Class Wide型を持つ副プログラムは継承されないのでエラー
end OOP4;
-- Derived_Procedure called.
-- CW_Procedure called.
-- Overrided Derived_Procedure called.
-- CW_Procedure called.
Class Wide型は自身と自身を継承する全ての型を許容する型だと言っていい。ただし、Class Wide型自体は派生される型そのものではないため、仮にパラメタに該当する型のClass Wide型が存在したとしても、その副プログラムは継承されない。上のコードの場合、Sub_Classes.CW_Procedureは存在しない。
その代わり、Class Wide型は1つの副プログラムで自身から派生した全ての型を受け入れることが出来るため、Super_Classes.CW_ProcedureのパラメータにSub_Class型の変数を与えてもエラーとは成らない。
Class Wide型を引数に取る副プログラムにおいても、実際のところオーバーライドは可能である。しかし、通常は継承されないClass Wide型を使用することにより、凡ミスによるメソッドの変更は防止できる、と、「Rational for Ada 2005」に書いてあるが、怪しい個人的には怪しいと考える。
むしろClass Wide型が活躍するのは後で説明するaccess型との組み合わせであろうと思われる。
なお、Class Wide型には派生型による継承のようなパッケージの制約がないので、このようなコードも書ける。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure OOP5 is
-- パッケージで分割しない
type Super_Record is tagged record
Val_X : Integer;
end record;
procedure Super_Method(O : Super_Record) is
begin
Put("Super_Method called."); New_Line;
end Super_Method;
type Sub_Record is new Super_Record with null record;
procedure Class_Wide_Super_Method(O : Super_Record'Class) is
begin
Put("Class_Wide_Super_Method called."); New_Line;
end Class_Wide_Super_Method;
Super : Super_Record;
Sub : Sub_Record;
begin
Super_Method(Super);
Class_Wide_Super_Method(Super);
-- Super_Method(Sub); -- パッケージがないので継承されていない
Class_Wide_Super_Method(Sub); -- Class Wide型なのでパッケージに関係なく受け入れられる
end OOP5
-- Super_Method called.
-- Class_Wide_Super_Method called.
-- Class_Wide_Super_Method called.
;
XML/Adaを使ってXMLな文章をDOMで操作できるようにしてみた。
恐らくインストールはしなくても動く。手元では一応configureのprefixをプロジェクトの場所に指定してmakeしてみた。
% tar -xzvf XmlAda-1.0.tgz % cd xmlada-1.0 % ./configure --prefix=/home/foo/project % make install
この方法でインストールした場合はREADMEに書かれているとおりコンパイル時にライブラリの場所を指定する必要がある。付属のスクリプトを使うと楽ちん。
% gnatmake sample `bin/xmlada-config`"
さて、ファイルを開いてDOMのDocumentオブジェクトを取得してみよう。
with Input_Sources.File;
with Sax.Readers;
with DOM.Readers;
with DOM.Core; use DOM.Core;
with DOM.Core.Elements; use DOM.Core.Elements;
with DOM.Core.Documents; use DOM.Core.Documents;
with DOM.Core.Nodes; use DOM.Core.Nodes;
with Sax.Encodings; use Sax.Encodings;
with Unicode.CES; use Unicode.CES;
with Unicode.CES.Basic_8bit; use Unicode.CES.Basic_8bit;
with DOM.Core.Texts; use DOM.Core.Texts;
with Ada.Text_IO; use Ada.Text_IO;
procedure Sample is
Xml_Input : Input_Sources.File.File_Input;
Xml_Reader : Dom.Readers.Tree_Reader;
Xml_Document : Document;
-- 文字列リテラルをDom_Stringに変換する関数
function "-" (Str : String) return Byte_Sequence is
begin
return Sax.Encodings.From_Utf32 (Unicode.CES.Basic_8bit.To_Utf32 (Str));
end "-";
begin
-- sample.xmlを開いて、1度SAXを通しつつ、DOM Documentに変換
Input_Sources.File.Open(Filename => "./sample.xml", Input => Xml_Input);
Sax.Readers.Parse(Parser => Sax.Readers.Reader(Xml_Reader), Input => Xml_Input);
Xml_Document := Dom.Readers.Get_Tree(Read => Xml_Reader);
-- ルート要素のhoge属性を取得してみる
-- ここまでくれば後はほぼいつも通り
Put(Get_Attribute(Get_Element(Xml_Document), -"hoge")); New_Line;
-- 閉じる
Dom.Readers.Free(Xml_Reader);
Input_Sources.File.Close(Xml_Input);
end Sample;
User's Guideには何も書かれていないので、素直にadsファイルを読むのが良い。ちなみに、一連のDOM Nodeはtagged recordではなくて、普通のrecordなので、Ada05でもドット記法は使えなさそう。