Adaのジェネリックプログラミング ── 型で契約を書き、再利用をゼロコストで実現する

· · Ada, ProgrammingLanguage, Generics, TypeSystem, StaticTyping, ContractModel, ZeroCostAbstraction, GNAT, Alire, 高信頼性, コード再利用

1. はじめに ── 「何でも受け取る」ではなく「何を約束するか」

静的型付き言語で再利用可能なコードを書こうとすると、すぐに同じ悩みにぶつかります。整数用に書いたスタックを文字列でも使いたい。浮動小数点の配列にも同じ統計処理を使いたい。昇順ソートのロジックを降順ソートにも使いたい。ところが、型ごとに同じコードをコピーしていくと、修正漏れが起きます。逆に void* やキャストで何でも受け取る設計にすると、型安全性が崩れます。

Ada の答えは ジェネリック(generic units) です。

Ada のジェネリックは、単なるテキスト置換ではありません。型、値、サブプログラム、さらにはパッケージそのものを仮パラメータとして受け取り、インスタンス化の時点で静的に型チェックされます。つまり、実行時に「この型で本当に大丈夫か」を調べるのではなく、コンパイル時に「この部品はこの契約を満たしているか」を確定させる仕組みです。

flowchart LR
    A[再利用したい処理] --> B{どう再利用するか}
    B --> C[コピーとペースト]
    B --> D[void* / Object / キャスト]
    B --> E[Ada ジェネリック]

    C --> C1[修正漏れが起きやすい]
    D --> D1[実行時エラーや型崩れが起きやすい]
    E --> E1[型安全]
    E --> E2[コンパイル時チェック]
    E --> E3[実行時の余分なディスパッチなし]

本記事では、Ada のジェネリックプログラミングを次の流れで整理します。

  • 総称サブプログラム
  • 総称パッケージ
  • 型パラメータ、値パラメータ、サブプログラムパラメータ
  • privaterange <>digits <> などの型カテゴリ
  • ソート、スタック、統計処理、Count_If、キーバリューストアの実装例
  • 正式パッケージパラメータによる高階ジェネリック
  • Ada の contract model と実務設計の考え方

このテーマは、当ブログの連載「Ada言語の魅力」「SPARKによる形式検証入門」「安全な並行処理」「リアルタイムシステム」に続く位置づけです。Ada の「型で設計を語る」考え方を、ジェネリックという切り口から掘り下げます。

2. この記事の地図

まず全体像を図で押さえます。Ada のジェネリックは「型を引数に取る機能」とだけ理解すると、かなり狭い見方になります。実際には、再利用したい単位に応じて、サブプログラム、パッケージ、サブプログラムパラメータ、値パラメータ、正式パッケージパラメータを組み合わせます。

mindmap
  root((Ada Generics))
    Generic Subprogram
      Swap
      Count_If
      Sort
    Generic Package
      Stack
      Statistics
      KV Store
    Formal Parameters
      Type
        private
        limited private
        range box
        mod box
        digits box
        delta box
        discrete box
      Object
        Max_Size
        Threshold
      Subprogram
        Less function
        Equals function
        Predicate
      Package
        with package P is new Generic
    Design Ideas
      Contract Model
      Static Checking
      Zero-Cost Abstraction
      Separate Specification and Body

この記事の読み方は単純です。前半では構文を追い、後半では設計判断を扱います。Ada を初めて読む方は、最初は細かな構文を覚えようとせず、「何を仮パラメータにしているのか」「その仮パラメータにどんな操作を許しているのか」に注目してください。

3. 実行環境とコンパイル方法

本記事のコードは GNAT 15.x 以降 を想定しています。GNAT は Ada の代表的なコンパイラで、Alire から導入できます。Alire は Ada / SPARK のパッケージマネージャであり、ツールチェーン管理やビルドにも使えます。

gnat --version
# GNAT 15.2.1

GNAT は Alire(Ada のパッケージマネージャ)から alr install gnat_native gprbuild で導入し、PATH を通してください。

本記事で扱うサンプルは、リポジトリ内で次のように置く想定です。

flowchart TB
    R[ada-generic-programming/] --> S[src/]
    S --> N[snippets/]
    N --> A[01_swap.ada]
    N --> B[02_stack.ada]
    N --> C[03_sort.ada]
    N --> D[04_statistics.ada]
    N --> E[05_filter.ada]
    N --> F[06_kv_store.ada]
    R --> README[README.md]

複数のコンパイル単位を 1 ファイルにまとめたサンプルは、gnatchop で分割してから gnatmake します。

mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo

-gnata はアサーションを有効にするオプションです。ジェネリックそのものを使うために必須ではありませんが、学習用のサンプルでは契約や境界条件を確認しやすくなります。

sequenceDiagram
    participant Dev as 開発者
    participant Chop as gnatchop
    participant Build as gnatmake
    participant Exe as 実行ファイル

    Dev->>Chop: 1つの .ada ファイルを渡す
    Chop-->>Dev: .ads / .adb / main に分割
    Dev->>Build: gnatmake -gnata main
    Build-->>Dev: バインドとリンクまで実行
    Dev->>Exe: ./main
    Exe-->>Dev: 実行結果

4. Ada ジェネリックの基本モデル

Ada のジェネリックは、大まかには次の 3 つで考えると分かりやすいです。

  1. 総称単位を書く
  2. generic 部分に仮パラメータを書く
  3. 使う側で new によりインスタンス化する
flowchart LR
    G[generic 宣言] --> F[仮パラメータ]
    F --> B[総称本体]
    B --> I[new によるインスタンス化]
    I --> U[通常のサブプログラムまたはパッケージとして使用]

    F --> F1[型]
    F --> F2[値]
    F --> F3[サブプログラム]
    F --> F4[パッケージ]

たとえば、2 つの値を入れ替える処理をジェネリックにすると、型だけを仮パラメータにできます。

generic
   type Element is private;
procedure Generic_Swap (A, B : in out Element);

この時点では、Generic_Swap はまだ呼び出せません。これは「任意の Element 型について使える入れ替え処理のテンプレート」です。具体的な型を与えることで、初めて通常のプロシージャになります。

procedure Swap_Integer is new Generic_Swap (Integer);

図にすると、次のような関係です。

flowchart TB
    Template[Generic_Swap<br/>type Element is private] -->|Integer を渡す| SwapInt[Swap_Integer]
    Template -->|Character を渡す| SwapChar[Swap_Character]
    Template -->|My_Record を渡す| SwapRecord[Swap_My_Record]

    SwapInt --> ICall[Integer 変数を入れ替える]
    SwapChar --> CCall[Character 変数を入れ替える]
    SwapRecord --> RCall[My_Record 変数を入れ替える]

重要なのは、テンプレート本体が Element に対して使える操作だけで書かれていることです。type Element is private; と宣言した場合、代入や等価比較などの基本的な操作は使えますが、大小比較や算術演算は使えません。つまり、ジェネリックの宣言そのものが「この部品は何を前提にしてよいか」を表します。

5. 仮パラメータの種類 ── Ada ジェネリックの語彙

Ada のジェネリックで受け取れるものは、型だけではありません。ここが C# や Java の一般的なジェネリクスと比べて大きく違う点です。

flowchart TB
    P[generic formal parameters] --> T[型パラメータ]
    P --> O[オブジェクト / 値パラメータ]
    P --> S[サブプログラムパラメータ]
    P --> PKG[パッケージパラメータ]

    T --> T1[type Element is private]
    T --> T2[type Index is box]
    T --> T3[type Real is digits box]
    O --> O1[Max_Size : Positive]
    O --> O2[Default_Value : Element]
    S --> S1[with function Less...]
    S --> S2[with procedure Put ...]
    PKG --> P1[with package P is new ...]

代表的な仮パラメータを表にすると、次のようになります。

種類 意味
型パラメータ type Element is private; 任意の definite な非 limited 型を受け取る基本形
limited 型パラメータ type Element is limited private; コピーできない型も受け取る
離散型 type Index is (<>); 整数型や列挙型など、配列添字に使える型
符号付き整数型 type Count is range <>; +-、大小比較など整数演算を前提にできる
モジュラー整数型 type Word is mod <>; ビット演算や剰余的な整数を扱う
浮動小数点型 type Real is digits <>; FloatLong_Float、ユーザ定義浮動小数点型など
固定小数点型 type Money is delta <>; 固定小数点演算を扱う
値パラメータ Max_Size : Positive; サイズや閾値などをインスタンスごとに固定する
サブプログラム with function Predicate (...) return Boolean; 比較関数や述語など、振る舞いを注入する
パッケージ with package P is new Some_Generic (<>); 生成済みのジェネリックパッケージを部品として受け取る

この語彙を持っているため、Ada では「何でも受け取るが、中で危険なことをする」ではなく、「この操作ができる型だけ受け取る」という書き方が自然にできます。

6. 総称サブプログラム ── Generic_Swap で最小構成を理解する

最初の例として、任意の型の 2 つの変数を入れ替える Generic_Swap を見てみます。

6.1 仕様部

generic
   type Element is private;
procedure Generic_Swap (A, B : in out Element);

generic に続く部分が仮パラメータです。ここでは Element という型を受け取ります。is private は、ジェネリック本体から見たときに、その型の内部表現を知らないという意味です。

この宣言で分かることは、次の 2 つです。

  • Generic_Swap は任意の Element 型に対して使える
  • 本体では、Element の内部構造や大小比較には依存しない

6.2 本体

procedure Generic_Swap (A, B : in out Element) is
   Temp : constant Element := A;
begin
   A := B;
   B := Temp;
end Generic_Swap;

この本体では、Element に対して代入だけを使っています。A < B も、A + B も使っていません。したがって、IntegerCharacter、レコード型、列挙型など、代入可能な型であれば自然に使えます。

flowchart LR
    subgraph Before[呼び出し前]
        A1[A = 10]
        B1[B = 20]
    end

    A1 --> T[Temp = A]
    B1 --> A2[A = B]
    T --> B2[B = Temp]

    subgraph After[呼び出し後]
        A2[A = 20]
        B2[B = 10]
    end

6.3 インスタンス化

使う側では new を使います。

procedure Swap_Int  is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);

これで Swap_IntSwap_Char は通常のプロシージャとして呼び出せます。

with Ada.Text_IO; use Ada.Text_IO;

procedure Swap_Demo is
   generic
      type Element is private;
   procedure Generic_Swap (A, B : in out Element);

   procedure Generic_Swap (A, B : in out Element) is
      Temp : constant Element := A;
   begin
      A := B;
      B := Temp;
   end Generic_Swap;

   procedure Swap_Int is new Generic_Swap (Integer);

   X : Integer := 10;
   Y : Integer := 20;
begin
   Put_Line ("Before: X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
   Swap_Int (X, Y);
   Put_Line ("After : X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
end Swap_Demo;

実行イメージは次の通りです。

Before: X= 10, Y= 20
After : X= 20, Y= 10

ここで Swap_Int (X, Y);Float の変数を渡すことはできません。Swap_IntInteger 用にインスタンス化された通常のプロシージャだからです。ジェネリックは「何でも入る穴」ではなく、「型ごとに安全な具体物を作る仕組み」だと捉えると理解しやすくなります。

7. 総称パッケージ ── 型と値をパラメータにする

サブプログラム 1 個ではなく、複数の操作と内部状態をまとめて再利用したい場合は、総称パッケージを使います。代表例はスタックです。

スタックは、要素型と最大サイズだけを変えれば、基本的なロジックは同じです。

flowchart TB
    G[Generic_Stack] --> P1[Element_Type]
    G --> P2[Max_Size]
    G --> Ops[Push / Pop / Size / Is_Empty / Is_Full]

    G --> I1[Int_Stack<br/>Element=Integer<br/>Max_Size=5]
    G --> I2[Float_Stack<br/>Element=Float<br/>Max_Size=3]
    G --> I3[String_Stack<br/>Element=Unbounded_String<br/>Max_Size=20]

7.1 仕様部

generic
   type Element_Type is private;
   Max_Size : Positive;
package Generic_Stack is
   procedure Push (Item : Element_Type);
   function Pop return Element_Type;
   function Is_Empty return Boolean;
   function Is_Full  return Boolean;
   function Size return Natural;

   Stack_Overflow  : exception;
   Stack_Underflow : exception;
end Generic_Stack;

ここでは 2 種類の仮パラメータを使っています。

  • Element_Type は型パラメータ
  • Max_Size は値パラメータ

Max_SizePositive なので、0 以下のサイズではインスタンス化できません。このように、値パラメータにも型による制約を持たせられます。

7.2 本体

package body Generic_Stack is
   subtype Index_Type is Positive range 1 .. Max_Size;
   type Storage_Type is array (Index_Type) of Element_Type;

   Data : Storage_Type;
   Top  : Natural := 0;

   procedure Push (Item : Element_Type) is
   begin
      if Top = Max_Size then
         raise Stack_Overflow;
      end if;

      Top := Top + 1;
      Data (Top) := Item;
   end Push;

   function Pop return Element_Type is
      Result : Element_Type;
   begin
      if Top = 0 then
         raise Stack_Underflow;
      end if;

      Result := Data (Top);
      Top := Top - 1;
      return Result;
   end Pop;

   function Is_Empty return Boolean is
   begin
      return Top = 0;
   end Is_Empty;

   function Is_Full return Boolean is
   begin
      return Top = Max_Size;
   end Is_Full;

   function Size return Natural is
   begin
      return Top;
   end Size;
end Generic_Stack;

このパッケージ本体で重要なのは、DataTop がインスタンスごとに別々に作られることです。

package Int_Stack   is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float,   3);

この 2 つは同じテンプレートから作られますが、内部状態は共有しません。

flowchart LR
    Template[Generic_Stack] --> IntStack[Int_Stack]
    Template --> FloatStack[Float_Stack]

    subgraph I[Int_Stack の状態]
        ITop[Top]
        IData[Data : Integer配列]
    end

    subgraph F[Float_Stack の状態]
        FTop[Top]
        FData[Data : Float配列]
    end

    IntStack --> I
    FloatStack --> F

7.3 スタックの状態遷移

スタックは状態機械として見ると分かりやすいです。

stateDiagram-v2
    [*] --> Empty
    Empty --> NonEmpty: Push
    NonEmpty --> NonEmpty: Push / Pop
    NonEmpty --> Empty: Popで最後の要素を取り出す
    NonEmpty --> Full: PushでMax_Sizeに到達
    Full --> NonEmpty: Pop
    Full --> Overflow: Push
    Empty --> Underflow: Pop

7.4 使用例

with Ada.Text_IO; use Ada.Text_IO;

procedure Stack_Demo is
   generic
      type Element_Type is private;
      Max_Size : Positive;
   package Generic_Stack is
      procedure Push (Item : Element_Type);
      function Pop return Element_Type;
      function Is_Empty return Boolean;
      function Is_Full  return Boolean;
      function Size return Natural;
      Stack_Overflow  : exception;
      Stack_Underflow : exception;
   end Generic_Stack;

   package body Generic_Stack is
      subtype Index_Type is Positive range 1 .. Max_Size;
      type Storage_Type is array (Index_Type) of Element_Type;

      Data : Storage_Type;
      Top  : Natural := 0;

      procedure Push (Item : Element_Type) is
      begin
         if Top = Max_Size then
            raise Stack_Overflow;
         end if;

         Top := Top + 1;
         Data (Top) := Item;
      end Push;

      function Pop return Element_Type is
         Result : Element_Type;
      begin
         if Top = 0 then
            raise Stack_Underflow;
         end if;

         Result := Data (Top);
         Top := Top - 1;
         return Result;
      end Pop;

      function Is_Empty return Boolean is (Top = 0);
      function Is_Full  return Boolean is (Top = Max_Size);
      function Size     return Natural is (Top);
   end Generic_Stack;

   package Int_Stack is new Generic_Stack (Integer, 5);
begin
   Int_Stack.Push (10);
   Int_Stack.Push (20);
   Int_Stack.Push (30);

   Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
   Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
   Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
   Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
end Stack_Demo;

総称パッケージは、実務では「小さなコンテナ」「固定長バッファ」「リングバッファ」「ログ用キュー」「ハードウェア抽象化層」などでよく効きます。特に Ada では、サイズを実行時に動かすよりも、型や値パラメータとして静的に固定する設計が高信頼システムと相性よく働きます。

8. 仮サブプログラムパラメータ ── 振る舞いを注入する

型だけを受け取っても、まだ表現できないことがあります。たとえばソートでは、要素型だけでなく「どちらを先に並べるか」という比較ロジックが必要です。

Ada では、この比較関数をジェネリックの仮パラメータにできます。

flowchart LR
    A[Generic_Insertion_Sort] --> T[Item_Type]
    A --> I[Index]
    A --> ARR[Item_Array]
    A --> CMP[比較関数]

    CMP --> ASC[標準の比較 を使う]
    CMP --> DESC[Greater を渡して降順]
    CMP --> CUSTOM[独自順序を渡す]

8.1 仕様部

generic
   type Item_Type is private;
   type Index is (<>);
   type Item_Array is array (Index range <>) of Item_Type;
   with function "<" (Left, Right : Item_Type) return Boolean is <>;
procedure Generic_Insertion_Sort (Items : in out Item_Array);

ここには 4 つの仮パラメータがあります。

  1. Item_Type:配列要素の型
  2. Index:配列添字の型
  3. Item_Array:実際の配列型
  4. "<":比較関数

type Index is (<>); は離散型を受け取ります。整数型だけでなく、列挙型も受け取れます。配列添字に Positive を使うだけでなく、Day のような列挙型を使えるのが Ada らしいところです。

with function "<" ... is <>;is <> は、実パラメータを省略した場合に、可視な標準演算子や適合する関数を使うという意味です。つまり、Integer のように既に < がある型なら、比較関数を明示しなくても使えます。

8.2 本体

procedure Generic_Insertion_Sort (Items : in out Item_Array) is
   J   : Index;
   Key : Item_Type;
begin
   if Items'Length <= 1 then
      return;
   end if;

   for I in Index'Succ (Items'First) .. Items'Last loop
      Key := Items (I);
      J := I;

      while J > Items'First and then Key < Items (Index'Pred (J)) loop
         Items (J) := Items (Index'Pred (J));
         J := Index'Pred (J);
      end loop;

      Items (J) := Key;
   end loop;
end Generic_Insertion_Sort;

挿入ソートは、大きな配列には向きませんが、ジェネリックの説明には向いています。比較関数だけを差し替えることで、同じループ構造を昇順にも降順にも使えるからです。

flowchart TB
    Start[未整列配列] --> Pick[左から順に Key を取り出す]
    Pick --> Compare{Key は直前要素より前? }
    Compare -->|Yes| Shift[直前要素を右へずらす]
    Shift --> Compare
    Compare -->|No| Insert[Key を挿入]
    Insert --> Done{最後まで処理?}
    Done -->|No| Pick
    Done -->|Yes| End[整列済み配列]

8.3 昇順と降順を同じ本体から作る

type Int_Array is array (Positive range <>) of Integer;

procedure Sort_Asc is new Generic_Insertion_Sort
  (Item_Type  => Integer,
   Index      => Positive,
   Item_Array => Int_Array);

function Greater (Left, Right : Integer) return Boolean is
  (Left > Right);

procedure Sort_Desc is new Generic_Insertion_Sort
  (Item_Type  => Integer,
   Index      => Positive,
   Item_Array => Int_Array,
   "<"        => Greater);

Sort_Asc は標準の < を使います。一方、Sort_Desc"<" => Greater として、比較関数を差し替えています。

flowchart LR
    Data[99, 3, 47, 12] --> A[Sort_Asc<br/>標準の比較]
    Data --> D[Sort_Desc<br/>Greater を比較関数として渡す]
    A --> AO[3, 12, 47, 99]
    D --> DO[99, 47, 12, 3]

この仕組みは、C++ で比較関数オブジェクトをテンプレート引数に渡す設計や、Rust でトレイト境界を使って順序を要求する設計に近いものです。ただし Ada では、仮サブプログラムパラメータとして「この形の関数を渡す」と明示します。

9. 型カテゴリ ── private より具体的な契約を書く

type T is private; は便利ですが、何でもできるわけではありません。private な型に対しては、四則演算や大小比較を当然のようには使えません。そこで Ada では、仮型パラメータにカテゴリを指定できます。

flowchart TB
    FormalType[Formal Type] --> Private[private]
    FormalType --> Limited[limited private]
    FormalType --> Discrete[discrete box: 離散型]
    FormalType --> Signed[range box: 符号付き整数]
    FormalType --> Modular[mod box: モジュラー整数]
    FormalType --> Float[digits box: 浮動小数点]
    FormalType --> Fixed[delta box: 固定小数点]
    FormalType --> Access[access 型]

    Discrete --> Enum[列挙型]
    Discrete --> Int[整数型]
    Float --> F1[Float]
    Float --> F2[Long_Float]
    Float --> F3[ユーザ定義浮動小数点型]

9.1 カテゴリを指定すると何が嬉しいのか

たとえば平均や分散を計算するには、加算、減算、乗算、除算が必要です。private 型ではこれらの演算を前提にできません。そこで、浮動小数点型に限定します。

generic
   type Real is digits <>;
   type Real_Array is array (Positive range <>) of Real;
package Generic_Statistics is
   function Mean (Values : Real_Array) return Real;
   function Variance (Values : Real_Array) return Real;
end Generic_Statistics;

type Real is digits <>; により、Real は浮動小数点型であると分かります。したがって、ジェネリック本体では +-*/ などを使えます。

9.2 本体

package body Generic_Statistics is
   function Mean (Values : Real_Array) return Real is
      Sum : Real := 0.0;
   begin
      if Values'Length = 0 then
         return 0.0;
      end if;

      for V of Values loop
         Sum := Sum + V;
      end loop;

      return Sum / Real (Values'Length);
   end Mean;

   function Variance (Values : Real_Array) return Real is
      M   : constant Real := Mean (Values);
      Sum : Real := 0.0;
   begin
      if Values'Length = 0 then
         return 0.0;
      end if;

      for V of Values loop
         declare
            D : constant Real := V - M;
         begin
            Sum := Sum + D * D;
         end;
      end loop;

      return Sum / Real (Values'Length);
   end Variance;
end Generic_Statistics;

9.3 FloatLong_Float で使う

type Float_Array is array (Positive range <>) of Float;
type Long_Array  is array (Positive range <>) of Long_Float;

package Float_Stats is new Generic_Statistics (Float, Float_Array);
package Long_Stats  is new Generic_Statistics (Long_Float, Long_Array);

同じ統計処理を、精度の異なる浮動小数点型に対して再利用できます。

flowchart LR
    Stats[Generic_Statistics<br/>Real is digits box] --> FS[Float_Stats]
    Stats --> LS[Long_Stats]
    Stats --> MS[My_Real_Stats]

    FS --> FCalc[Float で Mean / Variance]
    LS --> LCalc[Long_Float で Mean / Variance]
    MS --> MCalc[ユーザ定義 Real で Mean / Variance]

9.4 カテゴリ指定は「型レベルの仕様書」

カテゴリ指定は、単にコンパイラを黙らせるための構文ではありません。読み手に対しても「この部品は何を要求しているか」を伝える仕様書になります。

書きたい処理 向いている仮型 理由
入れ替え、保存、取り出し private 代入できればよい
コピーできないリソース管理 limited private 代入を前提にしない
配列添字、列挙状態の走査 (<>) FirstLastSuccPred を使える
整数の合計、カウンタ range <> 整数演算を前提にできる
ビットマスク、循環カウンタ mod <> モジュラー演算を前提にできる
平均、分散、数値計算 digits <> 浮動小数点演算を前提にできる
金額、制御量などの固定精度 delta <> 固定小数点演算を前提にできる

10. 述語注入 ── Count_If を Ada らしく書く

仮サブプログラムパラメータは、比較関数だけでなく、述語にも使えます。述語とは、値を受け取って Boolean を返す関数です。

C# の Func<T, bool>、Java の Predicate<T>、C++ のラムダや関数オブジェクトに近い役割を、Ada ではジェネリックの仮サブプログラムとして表現できます。

10.1 仕様部

generic
   type Element is private;
   type Index is (<>);
   type Array_Type is array (Index range <>) of Element;
   with function Predicate (Item : Element) return Boolean;
function Generic_Count_If (Arr : Array_Type) return Natural;

ここでは Predicateis <> を付けていません。標準で可視な述語関数があるわけではないので、使う側に必ず渡してもらう設計です。

10.2 本体

function Generic_Count_If (Arr : Array_Type) return Natural is
   Count : Natural := 0;
begin
   for Item of Arr loop
      if Predicate (Item) then
         Count := Count + 1;
      end if;
   end loop;

   return Count;
end Generic_Count_If;

処理の流れは単純です。

flowchart LR
    Arr[配列] --> Loop[各要素を走査]
    Loop --> P{Predicate(Item)?}
    P -->|True| Inc[Countを増やす]
    P -->|False| Skip[何もしない]
    Inc --> Next[次の要素へ]
    Skip --> Next
    Next --> Result[Countを返す]

10.3 偶数カウントと閾値カウント

type Int_Array is array (Positive range <>) of Integer;

function Is_Even (N : Integer) return Boolean is
  (N mod 2 = 0);

function Is_Large (N : Integer) return Boolean is
  (N > 50);

function Count_Even is new Generic_Count_If
  (Element    => Integer,
   Index      => Positive,
   Array_Type => Int_Array,
   Predicate  => Is_Even);

function Count_Large is new Generic_Count_If
  (Element    => Integer,
   Index      => Positive,
   Array_Type => Int_Array,
   Predicate  => Is_Large);

同じ走査ロジックから、条件だけが違う 2 つの関数を作れます。

flowchart TB
    G[Generic_Count_If] --> E[Count_Even<br/>Predicate = Is_Even]
    G --> L[Count_Large<br/>Predicate = Is_Large]

    Data[12, 7, 88, 3, 56, 91, 44, 19, 62] --> E
    Data --> L
    E --> ER[偶数の個数]
    L --> LR[50より大きい個数]

この例では、配列の走査、カウンタの管理、結果の返却はすべて共通です。一方で「何を数えるか」だけを関数として注入しています。これが Ada における高階的な設計の基本形です。

11. 複数パラメータの合成 ── 汎用キーバリューストア

現実の部品では、型パラメータ 1 つだけで済むことは多くありません。キーと値の型、キーの比較方法、最大件数など、複数の条件を組み合わせる必要があります。

ここでは、固定長の簡単なキーバリューストアを例にします。

flowchart TB
    KV[Generic_KV_Store] --> K[Key_Type]
    KV --> V[Value_Type]
    KV --> EQ[キー一致関数]
    KV --> M[Max_Entries]

    KV --> Ops[Put / Get / Contains]
    Ops --> Use1[設定値ストア]
    Ops --> Use2[小規模キャッシュ]
    Ops --> Use3[組み込み向け固定長辞書]

11.1 仕様部

generic
   type Key_Type is private;
   type Value_Type is private;
   with function "=" (Left, Right : Key_Type) return Boolean is <>;
   Max_Entries : Positive := 50;
package Generic_KV_Store is
   procedure Put (Key : Key_Type; Val : Value_Type);
   function Get (Key : Key_Type) return Value_Type;
   function Contains (Key : Key_Type) return Boolean;

   Key_Not_Found : exception;
   Store_Full    : exception;
end Generic_KV_Store;

このパッケージには 4 つの仮パラメータがあります。

パラメータ 種類 役割
Key_Type キーの型
Value_Type 値の型
"=" サブプログラム キーの一致判定
Max_Entries 最大エントリ数

Max_Entries には := 50 でデフォルト値を与えています。したがって、特に指定しなければ 50 件のストアになります。

11.2 本体

package body Generic_KV_Store is
   subtype Index_Type is Positive range 1 .. Max_Entries;

   type Key_Array   is array (Index_Type) of Key_Type;
   type Value_Array is array (Index_Type) of Value_Type;
   type Used_Array  is array (Index_Type) of Boolean;

   Keys   : Key_Array;
   Values : Value_Array;
   Used   : Used_Array := (others => False);

   function Find_Index (Key : Key_Type) return Natural is
   begin
      for I in Index_Type loop
         if Used (I) and then Keys (I) = Key then
            return I;
         end if;
      end loop;

      return 0;
   end Find_Index;

   function Find_Free return Natural is
   begin
      for I in Index_Type loop
         if not Used (I) then
            return I;
         end if;
      end loop;

      return 0;
   end Find_Free;

   procedure Put (Key : Key_Type; Val : Value_Type) is
      Pos : Natural := Find_Index (Key);
   begin
      if Pos = 0 then
         Pos := Find_Free;

         if Pos = 0 then
            raise Store_Full;
         end if;

         Used (Pos) := True;
         Keys (Pos) := Key;
      end if;

      Values (Pos) := Val;
   end Put;

   function Get (Key : Key_Type) return Value_Type is
      Pos : constant Natural := Find_Index (Key);
   begin
      if Pos = 0 then
         raise Key_Not_Found;
      end if;

      return Values (Pos);
   end Get;

   function Contains (Key : Key_Type) return Boolean is
   begin
      return Find_Index (Key) /= 0;
   end Contains;
end Generic_KV_Store;

この実装は線形探索なので、大量データ向けではありません。しかし、固定長・小規模・動的メモリ確保なしという性質が重要な場面では使いやすい形です。

sequenceDiagram
    participant App as 呼び出し側
    participant Store as Generic_KV_Storeインスタンス
    participant Data as Keys/Values/Used

    App->>Store: Put(Key, Value)
    Store->>Data: Find_Index(Key)
    alt 既存キーあり
        Store->>Data: Values(Pos) := Value
    else 新規キー
        Store->>Data: Find_Free
        Store->>Data: Keys(Pos) := Key
        Store->>Data: Values(Pos) := Value
        Store->>Data: Used(Pos) := True
    end
    App->>Store: Get(Key)
    Store->>Data: Find_Index(Key)
    Data-->>Store: Pos
    Store-->>App: Values(Pos)

11.3 インスタンス化例

with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;

procedure KV_Demo is
   package Int_String_Store is new Generic_KV_Store
     (Key_Type    => Integer,
      Value_Type  => Unbounded_String,
      Max_Entries => 10);
begin
   Int_String_Store.Put (1, To_Unbounded_String ("Ada"));
   Int_String_Store.Put (2, To_Unbounded_String ("SPARK"));

   if Int_String_Store.Contains (1) then
      -- Get (1) で値を取得できる
      null;
   end if;
end KV_Demo;

"=" は省略しています。Integer には標準の等価演算子があり、is <> によってそれが使われるからです。

もしキーが大文字小文字を無視した文字列などであれば、独自の等価関数を渡せます。

function Same_Key (Left, Right : Unbounded_String) return Boolean is
  (To_Lower (To_String (Left)) = To_Lower (To_String (Right)));

package String_Key_Store is new Generic_KV_Store
  (Key_Type    => Unbounded_String,
   Value_Type  => Integer,
   "="         => Same_Key,
   Max_Entries => 100);

12. 正式パッケージパラメータ ── ジェネリックをさらに部品化する

Ada のジェネリックでは、パッケージそのものを仮パラメータにできます。これを使うと、「あるジェネリックパッケージから作られたインスタンス」を、別のジェネリックの入力として扱えます。

flowchart LR
    GS[Generic_Stack] --> IS[Int_Stack]
    IS --> Logger[Generic_Stack_Logger]
    Logger --> Logged[Int_Stack_Logger_Instance]

12.1 スタックを受け取るロガー

たとえば、先ほどの Generic_Stack のインスタンスを受け取り、そのサイズを表示するロガーを作るとします。

generic
   with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
   procedure Print_Size;
end Generic_Stack_Logger;

本体は次のようになります。

with Ada.Text_IO; use Ada.Text_IO;

package body Generic_Stack_Logger is
   procedure Print_Size is
   begin
      Put_Line ("Stack size =" & Natural'Image (Stack.Size));
   end Print_Size;
end Generic_Stack_Logger;

使う側では、まずスタックを作り、そのスタックをロガーに渡します。

package Int_Stack is new Generic_Stack
  (Element_Type => Integer,
   Max_Size     => 10);

package Int_Stack_Logger is new Generic_Stack_Logger
  (Stack => Int_Stack);

このような設計により、ジェネリック部品同士を組み合わせられます。

flowchart TB
    subgraph Layer1[第1段階]
        T1[Generic_Stack] --> I1[Int_Stack]
    end

    subgraph Layer2[第2段階]
        T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
    end

    I1 --> T2
    I2 --> API[Print_Size]

C++ のテンプレートテンプレートパラメータに近い用途ですが、Ada では「このジェネリックパッケージのインスタンスを受け取る」と明示できます。大規模な Ada コードでは、コンテナ、アルゴリズム、ログ、検査、テスト補助などを分けて組み合わせるときに便利です。

13. Contract Model ── Ada ジェネリックのいちばん大事な考え方

Ada のジェネリックを理解するうえで重要なのが contract model です。

ジェネリック本体は、仮パラメータが約束した操作だけを使って書かなければなりません。たとえば type Element is private; しか宣言していないのに、Element に対して < を使うことはできません。< を使いたいなら、仮サブプログラムとして明示するか、型カテゴリをより具体的にする必要があります。

flowchart TB
    Spec[generic formal part<br/>契約] --> Body[generic body<br/>契約の範囲内で実装]
    Body --> Check1[本体単独で型チェック]
    Spec --> Inst[インスタンス化]
    Actual[actual parameters<br/>実型・実関数・実値] --> Inst
    Inst --> Check2[実パラメータが契約を満たすかチェック]
    Check2 --> Instance[通常のパッケージ/サブプログラム]

この設計により、ジェネリックの利用者だけでなく、ジェネリックを書く側も守られます。

13.1 C++ テンプレートとの見え方の違い

C++ のテンプレートは強力ですが、歴史的には「テンプレート本体をインスタンス化して初めてエラーが出る」という性質がありました。C++20 の concepts により改善されていますが、Ada のジェネリックは最初から契約を明示するモデルです。

flowchart LR
    subgraph Ada[Ada]
        A1[formal partに契約を書く] --> A2[bodyは契約内でチェック]
        A2 --> A3[instantiationでactualをチェック]
    end

    subgraph CPP[C++ templates]
        C1[template bodyを書く] --> C2[instantiation時に要求式が具体化]
        C2 --> C3[conceptsで制約を明示可能]
    end

Java や C# のジェネリクスは、参照型や制約、型消去、実行時表現との関係が設計の中心になります。一方、Ada のジェネリックは、コンパイル時に具体的なインスタンスを作る考え方に寄っています。

観点 Ada C++ Java Rust
契約の書き方 formal part に型・値・関数・パッケージを書く templates / concepts 型パラメータと bounds trait bounds
本体のチェック 仮パラメータの契約内でチェック インスタンス化時の具体化が中心 bounds 内でチェック trait bounds 内でチェック
実行時コスト 静的解決が基本 静的生成が基本 型消去の影響を受ける 単相化が基本
値パラメータ あり あり 限定的 const generics
サブプログラムを仮パラメータにする あり 関数オブジェクトなどで表現 ラムダ/関数型インターフェイス クロージャ/関数/trait
パッケージを仮パラメータにする あり テンプレートテンプレートなど なし モジュール構造とは別

細かい言語機能はそれぞれ違いますが、Ada の特徴は「契約を構文として先に書く」ことです。

14. 実務での設計判断 ── 何をジェネリックにするべきか

ジェネリックは便利ですが、何でもジェネリックにすればよいわけではありません。実務では、次のように判断すると失敗しにくくなります。

flowchart TB
    Start[再利用したい処理がある] --> Q1{型だけ違う?}
    Q1 -->|Yes| GType[型パラメータを検討]
    Q1 -->|No| Q2{サイズや閾値も違う?}
    Q2 -->|Yes| GObject[値パラメータを追加]
    Q2 -->|No| Q3{比較や判定の振る舞いが違う?}
    Q3 -->|Yes| GSubp[仮サブプログラムを追加]
    Q3 -->|No| Q4{内部状態やAPIをまとめたい?}
    Q4 -->|Yes| GPackage[総称パッケージ]
    Q4 -->|No| Normal[通常のサブプログラムで十分]

14.1 総称サブプログラムが向いている場面

総称サブプログラムは、状態を持たないアルゴリズムに向いています。

  • Swap
  • Sort
  • Count_If
  • Find
  • Map 的な変換
  • Min / Max

アルゴリズム本体が短く、入力と出力が明確な場合は、パッケージよりもサブプログラムのほうが読みやすくなります。

14.2 総称パッケージが向いている場面

総称パッケージは、型と一緒に複数の操作や内部状態を持ちたい場合に向いています。

  • 固定長スタック
  • リングバッファ
  • 小規模辞書
  • 統計処理セット
  • デバイスごとの I/O 抽象化
  • 単位系つき数値型の演算セット

特に Ada では、パッケージ仕様部が公開 API、パッケージ本体が実装という分離になっているため、ジェネリックパッケージは「型安全なモジュールテンプレート」として使えます。

flowchart LR
    Spec[package spec<br/>公開API] --> User[利用側]
    Body[package body<br/>内部実装] -.隠蔽.-> User
    Formal[generic formal part<br/>型・値・関数の契約] --> Spec
    Formal --> Body

14.3 仮パラメータは少なく始める

仮パラメータを増やしすぎると、インスタンス化が読みにくくなります。最初は最小限にし、差し替えたい理由が出たら増やすのが安全です。

-- 読みにくくなりやすい例
package X is new Generic_Foo
  (A, B, C, D, E, F, G);

-- 名前付き関連付けで意図を残す
package X is new Generic_Foo
  (Element_Type => Integer,
   Index_Type   => Positive,
   Buffer_Size  => 128,
   "<"          => Less);

Ada では、インスタンス化時に名前付き関連付けを使えます。ジェネリックは設計上の重要点がインスタンス化に現れるため、実務コードでは名前付きで書くほうが保守しやすいことが多いです。

15. よくあるつまずきどころ

Ada のジェネリックは強力ですが、最初につまずきやすい点があります。

15.1 private 型では大小比較できない

次のような本体は書けません。

generic
   type Element is private;
function Bad_Min (A, B : Element) return Element;

function Bad_Min (A, B : Element) return Element is
begin
   if A < B then      -- ここでエラー
      return A;
   else
      return B;
   end if;
end Bad_Min;

Elementprivate としか宣言されていないため、< が使えるとは限りません。比較したいなら、次のように契約へ追加します。

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
flowchart LR
    Need[本体で比較を使いたい] --> Contract[formal partに比較関数を書く]
    Contract --> OK[インスタンス化時に比較可能性を確認]
    Need --> NoContract[privateだけ]
    NoContract --> Error[ジェネリック本体でコンパイルエラー]

15.2 is <> は「何でも自動推論」ではない

is <> は便利ですが、魔法ではありません。インスタンス化地点で、適合する演算子やサブプログラムが可視である必要があります。独自の比較関数を別パッケージに置いた場合は、適切に withuse を行うか、名前付きで明示的に渡すのが安全です。

procedure Sort_By_Age is new Generic_Insertion_Sort
  (Item_Type  => Person,
   Index      => Positive,
   Item_Array => Person_Array,
   "<"        => Younger_Than);

15.3 インスタンスごとに例外も別物になる

ジェネリックパッケージの仕様部に例外を宣言すると、インスタンスごとに別の例外になります。

package Int_Stack   is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);

この場合、Int_Stack.Stack_OverflowFloat_Stack.Stack_Overflow は別の例外として扱われます。共通の例外として扱いたいなら、ジェネリックの外側に例外を定義する設計も検討します。

flowchart TB
    Generic[Generic_Stack<br/>Stack_Overflow宣言] --> I[Int_Stack.Stack_Overflow]
    Generic --> F[Float_Stack.Stack_Overflow]
    I -.別の例外.-> F

15.4 コードサイズが増えることがある

ジェネリックは実行時の余計な間接参照を避けやすい一方で、型ごとにインスタンスを作るため、インスタンス数が多いとコードサイズが増える可能性があります。

これは C++ のテンプレートや Rust の単相化でも見られるトレードオフです。高信頼・組み込み・リアルタイム寄りの開発では、実行時の不確実性を減らす代わりに、ビルド時の生成物サイズを管理するという考え方になります。

flowchart LR
    Generic[1つのgeneric本体] --> I1[Integer版]
    Generic --> I2[Float版]
    Generic --> I3[Long_Float版]
    Generic --> I4[My_Type版]

    I1 --> Code[生成コード]
    I2 --> Code
    I3 --> Code
    I4 --> Code

    Code --> Pros[実行時の型判定やボクシングを避けやすい]
    Code --> Cons[インスタンスが多いとサイズ増加に注意]

15.5 limited private を使うべき場面

type Element is private; は代入を前提にします。ファイルハンドル、ロック、デバイスハンドルのように、コピーされたくないものを扱う場合は limited private を検討します。

generic
   type Resource is limited private;
   with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);

コピーできない型を扱う設計では、値を保存するコンテナよりも、手続きを適用するアルゴリズムや、参照を明示する設計のほうが安全です。

16. 小さな設計パターン集

ここからは、実務でよく使う形を短く整理します。

16.1 比較可能な値にだけ Min を提供する

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;

function Generic_Min (A, B : Element) return Element is
begin
   if A < B then
      return A;
   else
      return B;
   end if;
end Generic_Min;
flowchart LR
    T[Element] --> C[比較関数が必要]
    C --> M[Generic_Min]
    M --> R[小さい方を返す]

16.2 閾値を値パラメータにする

generic
   type Count_Type is range <>;
   Threshold : Count_Type;
function Generic_Is_Over (Value : Count_Type) return Boolean;

function Generic_Is_Over (Value : Count_Type) return Boolean is
begin
   return Value > Threshold;
end Generic_Is_Over;

値パラメータは、実行時の設定値ではなく、インスタンスの性質として固定したい値に向いています。

16.3 出力手段を注入する

generic
   type Element is private;
   with procedure Put (Item : Element);
procedure Generic_Print_Twice (Item : Element);

procedure Generic_Print_Twice (Item : Element) is
begin
   Put (Item);
   Put (Item);
end Generic_Print_Twice;

この形にすると、標準出力、ログ、テスト用バッファなど、出力先を差し替えられます。

flowchart TB
    Print[Generic_Print_Twice] --> Put[Putを仮サブプログラムとして受け取る]
    Put --> Console[コンソール出力]
    Put --> Log[ログ出力]
    Put --> Test[テスト用バッファ]

16.4 配列の添字型を固定しない

Ada では配列の添字型も重要な型情報です。Positive 決め打ちにせず、必要に応じて添字型も仮パラメータにすると再利用性が上がります。

generic
   type Element is private;
   type Index is (<>);
   type Array_Type is array (Index range <>) of Element;
procedure Generic_Clear (Arr : in out Array_Type; Value : Element);

この設計なら、Positive 添字の配列だけでなく、列挙型添字の配列にも対応できます。

flowchart LR
    Index[Index is discrete] --> Positive[Positive range]
    Index --> Day[Day列挙型]
    Index --> State[State列挙型]
    Index --> Slot[独自整数型]

17. Ada らしい API にするためのチェックリスト

ジェネリックを書くときは、最後に次の観点で見直すと読みやすくなります。

flowchart TB
    C[Generic設計チェック] --> C1[仮パラメータは最小限か]
    C --> C2[必要な演算をformal partに明示したか]
    C --> C3[private / range / digits などのカテゴリは適切か]
    C --> C4[名前付き関連付けで読みやすくインスタンス化できるか]
    C --> C5[インスタンスごとの状態や例外を意識しているか]
    C --> C6[コードサイズ増加を許容できるか]
    C --> C7[テスト用インスタンスを用意しているか]

文章でまとめると、次の通りです。

  • 本体で使う操作は、必ず仮パラメータの契約として見える形にする。
  • private で足りるなら private にする。算術が必要なら range <>digits <> を使う。
  • 比較、ハッシュ、出力、変換など、型ごとに変わる振る舞いは仮サブプログラムにする。
  • サイズや閾値がインスタンスの性質なら、値パラメータにする。
  • 状態を持つなら総称パッケージ、状態を持たないなら総称サブプログラムをまず考える。
  • インスタンス化では、引数が多いほど名前付き関連付けを使う。
  • 例外や内部状態はインスタンスごとに独立することを前提に設計する。

18. サンプル全体の構成例

記事中のサンプルをファイルに分けるなら、次のような構成にすると読みやすくなります。

flowchart TB
    Root[ada-generic-programming] --> Src[src]
    Src --> G[generics]
    Src --> D[demos]

    G --> SwapSpec[generic_swap.ads]
    G --> SwapBody[generic_swap.adb]
    G --> StackSpec[generic_stack.ads]
    G --> StackBody[generic_stack.adb]
    G --> SortSpec[generic_insertion_sort.ads]
    G --> SortBody[generic_insertion_sort.adb]
    G --> StatsSpec[generic_statistics.ads]
    G --> StatsBody[generic_statistics.adb]
    G --> CountSpec[generic_count_if.ads]
    G --> CountBody[generic_count_if.adb]
    G --> KVSpec[generic_kv_store.ads]
    G --> KVBody[generic_kv_store.adb]

    D --> SwapDemo[swap_demo.adb]
    D --> StackDemo[stack_demo.adb]
    D --> SortDemo[sort_demo.adb]
    D --> StatsDemo[statistics_demo.adb]
    D --> CountDemo[count_if_demo.adb]
    D --> KVDemo[kv_demo.adb]

小さな記事用サンプルなら 1 ファイルにまとめて gnatchop するのも便利ですが、実務や長期保守を考えるなら、仕様部 .ads と本体 .adb を分けるほうが Ada らしい構成になります。

19. まとめ ── 型で再利用の境界を決める

Ada のジェネリックプログラミングは、「型に依存しないコードを書く機能」というだけではありません。むしろ本質は、再利用可能な部品が何を要求するのかを、型とサブプログラムと値の契約として明示することにあります。

flowchart LR
    Contract[契約を書く] --> Generic[ジェネリック本体を書く]
    Generic --> Instance[型・値・関数を渡してインスタンス化]
    Instance --> Safe[型安全に使う]
    Safe --> Reuse[コピーなしで再利用]

この記事で見たように、Ada のジェネリックでは次のものを仮パラメータにできます。

  • サブプログラム
  • パッケージ

さらに、型については privatelimited privaterange <>mod <>digits <>delta <>(<>) など、かなり細かいカテゴリを指定できます。これにより、ジェネリック本体は「できるかどうか分からない操作」に依存せず、契約に書かれた操作だけで安全に実装できます。

C や古い C++ の資産では、再利用のためにマクロ、void*、関数ポインタ、手書きの型分岐が使われることがあります。Ada のジェネリックは、それらの用途の多くを、型安全で読みやすい形に置き換えられます。特に、長期保守、組み込み、リアルタイム、高信頼性ソフトウェアでは、こうした「コンパイル時に境界を決める設計」が大きな価値を持ちます。

20. 関連する相談領域

合同会社小村ソフトでは、Windows アプリ開発、既存資産の調査・改修、COM / ActiveX / 32bit / 64bit の境界整理、技術相談・設計レビューなどを扱っています。Ada のような静的型付け・高信頼性寄りの設計だけでなく、既存の C/C++、C#、VB6、MFC、COM 資産をどう整理して延命・移行するかも、実務上よく近いテーマになります。

参考リンク

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る