整数で学ぶAdaの型

Adaには整数の型を作る方法がたくさんあります。
その違いを見ていきましょう。

新しく整数型を作る

Adaでは、必要に応じてどんどん新しい整数型を作る、これが基本となります。

type My_Int is range -100 .. 100;

暗黙のうちに、必要な演算子(関数)が用意されます。

type My_Int is -100 .. 100;
-- function "+" (Right : My_Int) return My_Int;
-- function "-" (Right : My_Int) return My_Int;
-- function "+" (Left, Right : My_Int) return My_Int;
-- function "-" (Left, Right : My_Int) return My_Int;
-- function "*" (Left, Right : My_Int) return My_Int;
-- function "/" (Left, Right : My_Int) return My_Int;
-- function "rem" (Left, Right : My_Int) return My_Int;
-- function "mod" (Left, Right : My_Int) return My_Int;
-- function "**" (Left, Right : My_Int) return My_Int;
-- function "abs" (Right : My_Int) return My_Int;
-- ...

これらの関数は概念上のものではなくしっかり存在していてPackage_Name."+"みたいな表記でも参照できますし、genericのパラメータにも使えます。
ただしコンパイラの出力には含まれないため、関数のアドレスは取れません。呼び出し規約はIntrinsicです。

新しくビット演算用の整数型を作る

modで宣言した型は常に符号なしになり、ビット演算用の操作が使えるようになります。
またオーバーフローのチェックはなされません。

type My_Unsigned is mod 256; -- 範囲は0 .. 255

暗黙のうちに、必要な演算子(関数)が用意されます。

type My_Unsigned is mod 256;
-- function "+" (Right : My_Int) return My_Int;
-- function "-" (Right : My_Int) return My_Int;
-- function "+" (Left, Right : My_Int) return My_Int;
-- function "-" (Left, Right : My_Int) return My_Int;
-- function "*" (Left, Right : My_Int) return My_Int;
-- function "/" (Left, Right : My_Int) return My_Int;
-- function "rem" (Left, Right : My_Int) return My_Int;
-- function "mod" (Left, Right : My_Int) return My_Int;
-- function "**" (Left, Right : My_Int) return My_Int;
-- function "abs" (Right : My_Int) return My_Int;
-- function "not" (Right : My_Int) return My_Int;
-- function "and" (Left, Right : My_Int) return My_Int;
-- function "or" (Left, Right : My_Int) return My_Int;
-- function "xor" (Left, Right : My_Int) return My_Int;
-- ...

別に2の乗数である必要はありません。

type My_Odd_Unsigned is mod 5; -- 範囲は0 .. 4

2の乗数の場合は範囲を超えた時の挙動は自明ですが、そうでない場合はどうなるのでしょう?
溢れた演算結果を"mod"した結果(modulo剰余)になります。

A : My_Odd_Unsigned := -2; -- 3
B : My_Odd_Unsigned := -1; -- 4
C : My_Odd_Unsigned := My_Odd_Unsigned'Mod (5); -- 0
D : My_Odd_Unsigned := My_Odd_Unsigned'Mod (6); -- 1

既存の整数型からの派生

型宣言の時にnewを付けて宣言すると、元の型からの派生になります。
これは整数型のための構文ではなくて、抽象データ型のための構文です。整数型にも使えるってだけです。

type My_New_Int is new My_Int;
type My_New_Int is new My_Int range 10 .. 20; -- 元の型の部分範囲型から派生

こうして作った派生型は、元の型とは暗黙の変換はされませんが、明示的に変換することは可能な関係になります。
また、元の型のプリミティブ(同じパッケージで宣言されている関数や手続き)がこの型用に継承されます。
My_New_Int用の演算子は、継承によって用意されることになります。

type My_New_Int is new My_Int;
-- function "+" (Right : My_New_Int) return My_New_Int; -- 中身はMy_Int用と同じ
-- ...

Intrinsicだろうがユーザー定義だろうが区別はありません。

package P1 is
   type My_Int is -100 .. 100;
   function Double (X : My_Int) return My_Int;
end P1;
package body P1 is
   function Double (X : My_Int) return My_Int is
   begin
      return 2 * X;
   end Double;
end P1;
package P2 is
   type My_New_Int is new P1.My_Int;
   -- function "+" (Right : My_New_Int) return My_New_Int;
   -- ...
   -- function Double (X : My_New_Int) return My_New_Int; -- これも付いてくる
end P2;

また、継承したプリミティブの実装が気に入らない場合はオーバーライドすることができます。

package P2 is
   type My_New_Int is new P1.My_Int;
   overriding function Double (X : My_New_Int) return My_New_Int;
end P2;
package body P2 is
   overriding function Double (X : My_New_Int) return My_New_Int is
   begin
      return X * X;
   end Double;
end P2;

"+"等の元が自動で用意された関数もオーバーライドできます。

なにか見覚えのある用語が出てきましたが……そうです。これは、多態(動的ディスパッチ)が無いだけでオブジェクト指向なのです。プリミティブはメソッドと言い換えればわかりやすいでしょうか。

注意点もオブジェクト指向と同じです。
派生は関連性があるものに留めて、多用は控えましょう。
Adaの解説でよくあるnew Integer range ...なんてのは悪い例です。
例えば、Interfaces.C.intは(実装依存ですが大抵の場合)new Integerで宣言されていますが、これは元のIntegerがCのintと対応がとれている型とわかっているからできることです。
特定用途のための整数型が欲しい時は、完全に新しい型を作るべきです。汎用の部分範囲が欲しい時は、汎用の整数型であるIntegerから派生するのではなくて、単にIntegerの部分範囲型を使いましょう。

また、プリミティブ扱いされてしまいますので、型を宣言したのと同じパッケージで、その型を取るが直接関係はない関数や手続きを宣言するのはやめましょう。

制約について説明を加えますと、制約は、狭くなる方向にしか追加できません。
継承階層が下るに従って、メソッドは増えていくため機能が追加されていくように感じますが、逆に型が表す範囲は狭まっていきます。
オブジェクト指向でis-a関係を考える時みたいに10 .. 20 is a 0 .. 100みたいに考えればわかりやすいでしょうか。

どうでもいいですがAda95のRationaleもこの間違いを犯しています

部分範囲型

最初に書いておきますと、subtypeは部分範囲型を作る構文ではなくて、型に別名を付ける構文です。

subtype My_Alias is My_Int; -- 単なる別名

さて、部分範囲型は、型に制約を明示したものです。
派生とは別物で、元の型と同じものです。構文上も元の型名に修飾を加えたものとして表記します。名前をつける必要はありません。
変数のサイズも元の型と同じだけ確保されます(pramga Pack;や内部表現節を明記すれば圧縮できます)。

subtype My_Sub_Int is My_Int range 2 .. 3; -- My_Intの部分範囲型に別名を付ける
Var : My_Int range 4 .. 5; -- 部分範囲型は無名で使える
Arr : array (My_Int range 5 .. 6) of T; -- 添字部分がMy_Intの部分範囲型
if Var in Arr'Range then -- Arr'RangeはMy_Int range 5 .. 6
   ...
end if;
for I in My_Int'(6) .. 7 loop -- ループ範囲がMy_Int range 6 .. 7に相当
   ...
end loop;

部分範囲型は元の型に制約のチェックを加えただけですので、元の型と代入互換性があります。

subtypeは単なる別名付けですのでプリミティブの再宣言は行われません。演算子等は元の型のものを使うことになります。

package P1 is
   type My_Int is range -100 .. 100;
end P1;
package P2 is
   subtype My_Sub_Int is P1.My_Int;
end P2;
X : P1 := P1."+" (1, 2);
-- Y : P2 := P2."+" (2, 3); -- エラー
Z : P2 := P1."+" (4, 5); -- OK

必要であれば演算子のほうも別名を作ってやればいいです。

package P1 is
   type My_Int is range -100 .. 100;
end P1;
package P2 is
   subtype My_Alias is P1.My_Int;
   function "+" (Left, Right : My_Alias) return My_Alias
      renames P1."+";
end P2;
X : P1.My_Int := P1."+" (1, 2);
Y : P2.My_Alias := P2."+" (2, 3); -- OK
Z : P2.My_Alias := P1."+" (4, 5); -- OK

列挙型の別名の作り方(オマケ)

列挙型の要素も、プリミティブな関数扱いですので、必要であれば関数宣言の構文で別名を作ることができます。

package P1 is
   type My_Enum is (E1, E2);
   -- function E1 return My_Enum;
   -- function E2 return My_Enum;
end P1;
package P2 is
   subtype My_Alias is P1.My_Enum;
   function E1 return My_Alias
      renames P1.E1;
end P2;
subtype True_Only is Boolean range True .. True;
function True return True_Only renames Standard.True;
subtype Alphabet is Character range 'A' .. 'Z';
function 'A' return Alphabet renames Standard.'A';
function 'B' return Alphabet renames Standard.'B';

余談ですが、列挙型の要素が単なる関数であることと、Adaは返値だけでもオーバーロードが解決できることを把握していれば、列挙型の要素名の衝突を恐れる必要がなくなります。(プレフィクスや専用の名前空間が要らない)

type Color is (Red, Blue, Orange);
type Fruit is (Apple, Grape, Orange);
X : Color := Orange;
Y : Fruit := Orange;

access型の部分範囲型(オマケ)

access型にも整数型と同様の制約を追加できます、といっても、nullを含むか含まないかの一種類のみです。

type My_Ptr is access Integer;
subtype My_Not_Null_Ptr is not null My_Ptr;

Standardパッケージで宣言されている整数型

AdaではIntegerは、Standardという全ての翻訳単位から可視になっている特殊なパッケージ(誤解を恐れず書けばPascalのSystem、O'CamlのPervasives、HaskellのPrelude、Dのobjectのようなもの)で宣言されているだけの整数型です。

Integer

AdaではIntegerは、Standardパッケージで宣言されているだけの整数型です。
ユーザーが適当に宣言した型との間に違いはありません。
環境によってはShort_Short_Integer/Short_Integer/Long_Integer/Long_Long_Integerみたいなバリエーションがありますが、重要な型ではありません。
真面目にAdaするなら、用途別に範囲を明示した新しい型を作りましょう。Integerを使うのは、特に用途を絞れない汎用の整数型が欲しい場合のみです。

Integer自体は特殊な型では無いのですが、型が確定できない場合にIntegerということになる場面が幾つかあり、そういう意味では優遇されています。(後述)

Interfaces.Cで定義されている型を使わなくても、Short_Short_Integerはsigned char、Short_Integerはshort、Integerはint、Long_Integerはlong、Long_Long_Integerはlong longと同じ範囲と思って間違いありません。

Natural/Positive

Standardパッケージで宣言されているIntegerの部分範囲型です。

subtype Natural is Integer range 0 .. Integer'Last;
                   -- "natural"は自然数を意味しますが、0を含んでいます
                   -- 学校で習う定義とは違うかもしれません
subtype Positive is Integer range 1 .. Integer'Last;
                   -- "positive"は正の数の意味です

Adaでは配列を1から始めることが普通ですので、添字としてPositiveはよく使いますし、1から始まる配列の長さが0の場合は'Firstが1、'Lastが0になりますので、'Lastを格納するための変数としてNaturalもよく使います。(1から始めるのが普通なだけで、配列の添字自体は0からでも-100からでも好きにできます)

ちなみにNegativeという型は標準にはありません。

Boolean/Character(オマケ)

Boolean/Characterも、Standardパッケージで宣言されているだけの列挙型です。
特殊な型ではありません。

type My_Char is ('A', 'B', 'C'); -- これコンパイル通ります
type My_Odd_Char is (E1, False, 'A', 'B', 'C'); -- 混ぜてもいいです

Booleanには追加で論理演算子がありますが、これも同じものは作れますしね。

type My_Bool is (False, True);
function "not" (Right : My_Bool) return My_Bool is
begin
   case Right is
      when True => return False;
      when False => return True;
   end case;
end "not";

ただしBooleanはif/while等の各種構文で要求されるという点では特別な型です。
and then/or elseのショートサーキット評価も、(関数としての実体のある演算子ではなく)Boolean専用の構文です。

アドレス演算用の整数型

64ビットマシーンでもintが32ビットの処理系があるように、アドレス演算用の整数型は別途必要になります。
Cではsize_tやptrdiff_tやintptr_t等ですね。
Adaではアドレス演算用の型はSystem.Storage_Elementsパッケージで宣言されています。
またメモリ上の最小単位として、Storage_Element型が宣言されています。

入出力用の整数型

だがちょっと待って欲しい。CPU内部で使われている最小単位と入出力の最小単位が同じだなんて誰が決めた?
9ビットCPUであっても、一般的なディスクを接続したらやっぱり8ビットが最小単位になるでしょう?とばかりに、Adaではメモリ上の最小単位と入出力の最小単位は違う型になっています。
入出力の最小単位は、Ada.StreamsパッケージでStream_Element型として宣言されています。
同パッケージのStream_Element_OffsetはCで言えばoff_tですね。

Universal_Integer

型がわからない整数定数の概念上の型。
Universal_Integerという型は存在しません。RMだけで使われている用語です。
Universal_Integerから任意の整数型へは暗黙に変換されます。

ええとすみません、厳密に言えばもうひとつRoot_Integerという概念上の型がありまして、でもUniversal_Integerとの違いとかややこしいのでここでは全部Universal_Integerで通させてください。

Universal_Integerではない例。

X : Integer := 100; -- この100はIntegerとして解釈されます
Y : Integer := 10 + 20; -- Integer用にオーバーロードされた"+"が使われて
                        -- Integer用"+"が引数にIntegerを要求するため
                        -- この10と20もIntegerとして解釈されます
Z : Integer := Integer (Long_Integer'(123));
               -- Long_Integer型として明示されている定数123を
               -- Integer型に変換しています(誤解を恐れずCで書けば(int)123L)

Universal_Integerになる例。

U : constant := 1234; -- UはUniversal_Integerになります
V : constant := 5 + 7; -- Universal_Integer用の"+"が使われます
-- このUやVは整数定数が書けるところなら型を問わずどこにでも使えます
W : Character := Character'Val (10); -- 'Valは整数でさえあれば引数にできるため
                                     -- この10は型付けされないままになります

配列とforループだけは例外です。

declare
   A : array (X .. Y) of T; -- 通常A'Rangeは(XやYの型) range X .. Yになります
   B : array (1 .. 10) of T; -- B'RangeはUniversal_Integerではなく
begin                        -- Integer range 1 .. 10になります
   ...
end;
for I in X .. Y loop -- 通常ループ変数Iの型はXやYの型になります
   ...
end loop;
for I in 1 .. 10 loop -- IはUniversal_IntegerではなくIntegerになります
   ...
end loop;

↑この規則をRM/AARMで探したのですが、配列の方は3.6(18)としてforループの方が見つかりませんでした……実際のコンパイラの動作はこうなっているのですが。情報求む。