RecordはとばしてTask。
もっとも基本的なTaskの使い方はprocedureなどの宣言部で定義を行う方法だ。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Calendar; use Ada.Calendar;
procedure Sample is
procedure Start_Task is
task Task1;
task body Task1 is
begin
Put_Line("Task1 BEGIN");
end Task1;
task Task2;
task body Task2 is
begin
Put_Line("Task2 BEGIN");
end Task2;
begin -- ここでTaskの処理がはじまる
Put_Line("Start_Task BEGIN"); -- すべてのTaskが開始されるまで待たされる
end Start_Task;
begin
Start_Task;
Put_Line("Tasks are DONE"); -- Start_Taskが完了するまで待たされる。
end Sample;
このコードの実行結果は、なにかイレギュラーな要素がない限り次のようになる。
Task1 BEGIN Task2 BEGIN Start_Task BEGIN -- Task1, Task2より後にきている Tasks are DONE
宣言部で単純にTaskを定義した場合、そのTaskの開始は親単位(つまり、そのタスクを宣言部に持つ関数とか)がbeginした瞬間(「直前」といった方がわかりやすいかもしれない)となる。上記のコードのようにタスクが複数(Task1, Task2)有る場合は次のような感じで実行されてていく。
親単位の宣言部が確立された段階で各タスクのメモリは確保されるが、実行はされない。親単位がbeginされると同時に、各タスクの宣言部が順番に確立され、beginされていく。並列(平行)処理が行われるのはここからで、Task1の確立が完了しbeginされると、Task1の処理と同時にTask2の確立が開始される。
なお、このとき注意すべきなのは、すべてのTaskが開始されるまで、親単位の並列実行は待たされるということだ。上の実行結果を見れば分かるとおり、一見1番最初に出力されそうな「Start_Task BEGIN」がTask1とTask2の後に来ていのが分かるだろう。
また、親単位が制御を「親の親」に戻すのは、自分の持つすべてのTaskが完了してからである。逆に言えば、「親の親」に制御が戻った時点で孫Taskがすべて完了していることが保証されるので、Taskがすべて完了したことをいちいち確認する必要がない。いわゆる「待ち合わせ」を簡単に行うできる、といえるだろう。上のコードでは「Tasks are DONE」が表示された時点で、Task1、Task2が完了していることが保証されている。
閑話休題。
それにしても、
select
accept Receive do
Do_Something;
end Receive;
else
null;
end select;
ってもっと簡単に書けないのかなぁ。「キューが空の時は待たないでスルー」って結構使うと思うんだけどな。
if Receive'Count > 0 then
accept Receive do
Do_Something;
end Receive;
end if;
これでもいけるけど、やっぱり複雑だ。
それ、Adaならできるよ。略して「それいだ」(何
Adaなら83の時代から部分集合余裕だぜー。ていうか、JavaってなんでJ2SE5まで列挙型なかったんだろね。気持ちは何となく分かるけども。
Ascii_StringはString型がまんまだし、Non_Negative_IntegerもやっぱりPositive型が最初からIntegerのsubtypeとして定義されているのでそれを使えばよし。
Scoreはこんな感じかな。派生型を使ってIntegerとの演算を禁止してるけど、状況によってはやり過ぎかも。どうせならScore_nも派生型にして、100と1000の間での演算を禁止してもいいかな。めんどくさい場合はIntegerのsubtypeにしちゃえば、型変換が不要なので楽ちん。
type Score is new Integer range 1 .. Integer'Last;
subtype Score_100 is Score range Score'First .. 100;
subtype Score_1000 is Score range Score'First .. 1000;
で、ふと気がついたんだけど、
type Non_Negative_Integer renames Positive;
って認められないのね。普通使わないからいいけど、packageをrenamesできるんだから、typeでもできるといいな。
次、UserIDの話。
Ada95は勉強中だから若干自信ないけど、インスタンス変数の実際のところであるtagged recordはどうやらrecordと同じく要素の内容を実行時に変化できるらしいので、こんな感じで行ける。Javaでいうとコンストラクタ次第でインスタンス変数が変化する感じ? なのかな。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
procedure Sample is
package User_ID is
type ID_Type is (Number, Handle);
type Object(T : ID_Type) is tagged record
case T is
when Number =>
Number : Integer;
when Handle =>
Handle : Unbounded_String;
end case;
end record;
end User_ID;
-- User_IDのサブクラスなら何でも入るポインタ
type User_ID_Ptr is access User_ID.Object'Class;
package User_Handle is
-- 制約はここで注入
type Object is new User_ID.Object(T => User_ID.Handle) with null record;
end User_Handle;
package User_Number is
-- 制約はここで注入
type Object is new User_ID.Object(T => User_ID.Number) with null record;
end User_Number;
X : User_ID_Ptr;
Y : User_ID_Ptr;
begin
-- User_IDを使う場合(その場でHandleかNumberかを決められる)
X := new User_ID.Object(T => User_ID.Number);
X.Number := 1000;
Y := new User_ID.Object(T => User_ID.Handle);
Y.Handle := To_Unbounded_String("Foo");
Put(X.Number); New_Line;
Put(To_String(Y.Handle)); New_Line;
-- 1000
-- Foo
-- User_HandleとUser_Numberを使う場合
X := new User_Handle.Object;
X.Handle := To_Unbounded_String("Bar");
Y := new User_Number.Object;
Y.Number := 2000;
Put(To_String(X.Handle)); New_Line;
Put(Y.Number); New_Line;
-- Bar
-- 2000
end Sample;
恐ろしくすっきりと書けますな。やはりAdaは最強ですね!
整数型とIntegerの派生型(Derived Type)の違いと部分型(subtype)の違い。微妙に分かりにくいので解説しながら整理してみる。
まず、部分型はその通り、親の型の1部だけを抜き取って型として定義する場合に使います。IntegerとPositiveですね。部分型の場合、同じグループに属していると見なされるので、IntegerにPositiveを代入することができます。部分型の制約に違反しないかがいり、あまり型については気にしないでいいでしょう。制約は緩めと言えます。
つぎに整数型ですが、これは「整数を値に持つ何か」です。「何か」であって「整数」それ自体ではないと思うのがいいかもしれません。たとえば1月から12月までを
type Month is range 1 .. 12;
などと定義することがよくあります。単純なInteger型とは厳密に区別されますし、
type Day is range 1 .. 31;
別に定義された他の整数型とも区別されます。具体的にいえば異なる整数型間での演算はできません。もっとも、「月」と「日付」を足し合わせる、などということはあり得ないことなので、もしそのようなコードがあればそれはおそらくバグでしょう。数値型が厳密に区別されるおかげで、コンパイルの段階でそういったバグを検出することができます。どうしても代入などを行いたい場合は明示的な型変換を行う必要があります。
最後に派生型ですが、整数型と同様、派生元の型と派生されたは厳密に区別されます。さらに、派生型の特徴の1つに、派生元の型が定義されているパッケージにおいて、その型を引数もしくは返値に持つ全てのサブプログラム(procedureやfunction)が定義されていれば、派生した型に同じサブプログラムが継承される、という点があります。この性質はとても強力で、様々な処理を持つ型を違う目的で使う際にとても役に立ちます。逆にIntegerが定義されているStandardパッケージには、Integerに関するサブプログラムがないため、実際のところIntegerからの派生は余り意味がありません。
というわけで、分かりにくいサンプルプログラムです。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure Sample is
-- Standard.Integer にはサブプログラムがないので、 改めて Integerを作る。
package Nise is
type Nise_Integer is range Integer'First .. Integer'Last;
procedure Procedure_For_Nise_Intger (X : Nise_Integer);
end Nise;
package body Nise is
procedure Procedure_For_Nise_Intger (X : Nise_Integer) is
begin
Ada.Integer_Text_IO.Put(Integer(X)); New_Line;
end Procedure_For_Nise_Intger;
end Nise;
use Nise;
-- 部分型による正の数の表現
subtype S_Positive is Nise_Integer range 1 .. Nise_Integer'Last;
-- 数値型による正の数の表現
type I_Positive is range 1 .. Nise_Integer'Last;
-- 派生型による正の数の表現
type D_Positive is new Nise_Integer range 1 .. Nise_Integer'Last;
Int_X : Nise_Integer;
S_Pos : S_Positive;
I_Pos : I_Positive;
D_Pos : D_Positive;
begin
Int_X := 10;
S_Pos := 20;
I_Pos := 30;
D_Pos := 40;
S_Pos := Int_X; -- 部分型なのでOK
-- I_Pos := Int_X; -- 区別されるので NG
I_Pos := I_Positive(Int_X); -- 型変換しているので OK
-- D_Pos := Int_X; -- 区別されるので NG
D_Pos := D_Positive(Int_X); --型変換しているので OK
Procedure_For_Nise_Intger(S_Pos); -- 部分型なのでOK
-- Procedure_For_Intger(I_Pos); -- 区別されるので NG
Procedure_For_Nise_Intger(D_Pos); -- 派生型ではサブプログラムが継承されるのでOK
end Sample;
MITのSMILE(Piggy Bankを作ってるグループ)が公開しているAjaxなTimelineに、blogのエントリーを並べるデモ。SPARQLとかXSLTとか。