Generische Programmierung in Ada ── Verträge in Typen schreiben und Wiederverwendung ohne Laufzeitkosten

· · Ada, Programmiersprache, Generics, Typsystem, Statische Typisierung, Vertragsmodell, Zero-Cost-Abstraktion, GNAT, Alire, Hohe Zuverlässigkeit, Codewiederverwendung

1. Einleitung ── Nicht „nimmt alles an“, sondern „was wird zugesichert“

Wer in einer statisch typisierten Sprache wiederverwendbaren Code schreiben will, stößt schnell auf dasselbe Problem. Einen für Ganzzahlen geschriebenen Stack möchte man auch für Zeichenketten nutzen. Dieselbe statistische Verarbeitung soll auch für Gleitkomma-Arrays gelten. Die Logik einer aufsteigenden Sortierung soll auch für eine absteigende Sortierung gelten. Kopiert man denselben Code aber für jeden Typ, entstehen leicht vergessene Korrekturen. Baut man umgekehrt ein Design, das mit void* oder Casts alles annimmt, bricht die Typsicherheit.

Adas Antwort darauf sind Generics (generic units).

Ada-Generics sind keine bloße Textersetzung. Sie nehmen Typen, Werte, Unterprogramme und sogar ganze Pakete als formale Parameter entgegen und werden zum Zeitpunkt der Instanziierung statisch typgeprüft. Es wird also nicht zur Laufzeit geprüft, „ob dieser Typ wirklich passt“, sondern bereits zur Kompilierzeit festgelegt, „ob diese Komponente diesen Vertrag erfüllt“.

Wiederzuverwendende LogikWie wiederverwenden?Kopieren und Einfügenvoid* / Object / CastAda GenericsVergessene Korrekturen wahrscheinlichLaufzeitfehler und Typverlust wahrscheinlichTypsicherPrüfung zur KompilierzeitKein zusätzliches Dispatching zur Laufzeit

Dieser Artikel ordnet die generische Programmierung in Ada in folgender Reihenfolge:

  • Generische Unterprogramme
  • Generische Pakete
  • Typparameter, Wertparameter, Unterprogrammparameter
  • Typkategorien wie private, range <>, digits <>
  • Implementierungsbeispiele: Sortierung, Stack, Statistik, Count_If, Key-Value-Store
  • Höherstufige Generics über formale Paketparameter
  • Adas Contract Model und praktische Entwurfsüberlegungen

1.1 Zielgruppe und was dieser Artikel bringt

Dieser Artikel richtet sich an Leserinnen und Leser wie diese:

  • Personen mit Erfahrung in C++-Templates, C#- oder Java-Generics, oder Rust-Generics
  • Personen, die die Ada-Syntax nicht im Detail kennen, sich aber für den Entwurfsgedanken „Verträge in Typen schreiben“ interessieren
  • Personen, die im Bereich Langzeitpflege, Embedded oder hoher Zuverlässigkeit über Entwurfsrichtlinien für wiederverwendbare Bausteine nachdenken

Für den Ada-Syntaxteil genügt es, Pakete (die Trennung von Spezifikation .ads und Rumpf .adb) sowie die Parametermodi in / out / in out zu kennen. Wer sich bei diesen beiden Punkten unsicher ist, liest am besten zunächst „Der Reiz der Sprache Ada“ - das beschleunigt das Verständnis.

Was Sie mitnehmen, ist weniger die Ada-Syntax selbst als die Entwurfshaltung, auszuformulieren, was eine wiederverwendbare Komponente voraussetzt, bevor man sie implementiert - und zwar als Typ. Dieser Gedanke lässt sich unmittelbar übertragen, wenn Sie entscheiden, wie weit Sie C#-Schnittstellenbeschränkungen oder C++20-Concepts treiben.

1.2 Leseleitfaden ── Sie müssen nicht alles lesen

Dieser Artikel hat 20 Kapitel. Sie müssen ihn nicht durchgehend lesen, sondern können je nach Ziel gezielt herauspicken.

Ziel Zu lesende Kapitel
Nur den Grundgedanken kurz erfassen Kapitel 4 (Grundmodell) → Kapitel 6 (minimales generisches Unterprogramm) → Kapitel 13 (Contract Model)
Selbst Bausteine schreiben Kapitel 4 → Kapitel 6 → Kapitel 7 (generisches Paket) → Kapitel 8 (Verhalten injizieren) → Kapitel 9 (Typkategorien)
Nur Leitlinien für Entwurfsentscheidungen Kapitel 13 → Kapitel 14 (was wird generisch) → Kapitel 15 (Stolperfallen) → Kapitel 17 (Checkliste)
Selbst ausprobieren Umgebung in Kapitel 3 einrichten, dann von den fertigen Beispielen in Kapitel 6 und 7 aus

Die kürzeste Route sind die drei Kapitel 4, 6 und 13. In diesen dreien stehen bereits formale Parameter, Instanziierung und Contract Model als Kern beisammen. Kapitel 5 und 9 sind Übersichten der formalen Parameter - Sie können sie bei Bedarf einfach als Nachschlagewerk konsultieren.

Dieses Thema knüpft an unsere Blogreihe „Der Reiz der Sprache Ada“, „Einführung in die formale Verifikation mit SPARK“, „Sichere Nebenläufigkeit“ und „Echtzeitsysteme“ an. Adas Denkweise, „Design in Typen auszudrücken“, wird hier über den Blickwinkel der Generics vertieft.

2. Die Landkarte dieses Artikels

Zunächst ein Überblick als Grafik. Versteht man Adas Generics nur als „Funktion, die einen Typ als Argument nimmt“, ist das eine ziemlich enge Sichtweise. Tatsächlich kombiniert man je nach der wiederzuverwendenden Einheit Unterprogramme, Pakete, Unterprogrammparameter, Wertparameter und formale Paketparameter.

Ada GenericsGeneric SubprogramSwapCount_IfSortGeneric PackageStackStatisticsKV StoreFormal ParametersTypeprivatelimited privaterange boxmod boxdigits boxdelta boxdiscrete boxObjectMax_SizeThresholdSubprogramLess functionEquals functionPredicatePackagewith package P is newGenericDesign IdeasContract ModelStatic CheckingZero-Cost AbstractionSeparate Specification andBody

Das box im Diagramm steht für Adas <> (den „box compound delimiter“). Da Mermaid <> nicht direkt darstellen kann, wird im Diagramm ausschließlich box geschrieben.

Der Weg durch diesen Artikel ist einfach. In der ersten Hälfte geht es um Syntax, in der zweiten um Entwurfsentscheidungen. Wer Ada zum ersten Mal liest, sollte sich nicht bemühen, sich alle Syntaxdetails einzuprägen, sondern darauf achten, „was als formaler Parameter dient“ und „welche Operationen für diesen formalen Parameter erlaubt sind“.

2.1 Mini-Wörterbuch der Begriffe

Begriffe, die im weiteren Verlauf immer wieder vorkommen, stellen wir vorab einander gegenüber. In der japanischen Ada-Literatur wird „generic“ oft mit „総称“ (etwa: „generisch/allgemein“) übersetzt; im Deutschen verwenden wir hier durchgehend „generisch“ beziehungsweise die englischen Fachbegriffe, wo sie im Sprachgebrauch üblich sind.

Bezeichnung in diesem Artikel Englisch Bedeutung
Generische Einheit generic unit Eine mit generic beginnende Deklaration. Sammelbegriff für generische Unterprogramme und generische Pakete
Formaler Parameter generic formal parameter Das zwischen generic und dem Deklarationsrumpf stehende Argument der empfangenden Seite. Entweder ein Typ, ein Wert, ein Unterprogramm oder ein Paket
Formal part generic formal part Der Teil, in dem die formalen Parameter aufgereiht sind. Man kann ihn auch als „die Stelle, an der der Vertrag steht“ lesen
Aktueller Parameter generic actual parameter Der Typ, Wert, das Unterprogramm oder Paket, das bei der Instanziierung tatsächlich übergeben wird
Instanziierung instantiation Das Erzeugen eines gewöhnlichen Unterprogramms oder Pakets aus einer generischen Einheit mittels new
box <> Die Bezeichnung für das Symbol <> in Ada. Drückt, wie in range <>, aus, dass „der konkrete Typ erst bei der Instanziierung festgelegt wird“
Contract Model contract model Adas Methode, den Rumpf ausschließlich innerhalb der im formalen Parameter zugesicherten Operationen zu schreiben und ihn eigenständig typzuprüfen (Kapitel 13)

Im Fließtext wird, wie in der Ada-Notation üblich, range <> und digits <> geschrieben. Nur in den Diagrammen steht range box und digits box, weil Mermaid Labels mit <> nicht direkt darstellen kann - die Bedeutung ist dieselbe wie im Fließtext. Wie die obige Tabelle zeigt, nennt Ada selbst <> bereits „box“, sodass die Diagramm-Notation nicht von Adas eigener Terminologie abweicht.

3. Ausführungsumgebung und Kompilierung

Der Code dieses Artikels setzt GNAT 15.x oder neuer voraus. GNAT ist der bekannteste Ada-Compiler und lässt sich über Alire installieren. Alire ist der Paketmanager für Ada / SPARK und lässt sich auch für die Verwaltung der Toolchain und das Bauen nutzen.

gnat --version
# GNAT 15.2.1

Installieren Sie GNAT über Alire (den Ada-Paketmanager) mit alr install gnat_native gprbuild und nehmen Sie es in PATH auf.

Die Beispiele dieses Artikels sind so gedacht, dass sie im Repository folgendermaßen abgelegt werden.

ada-generic-programming/src/snippets/01_swap.ada02_stack.ada03_sort.ada04_statistics.ada05_filter.ada06_kv_store.adaREADME.md

Beispiele, die mehrere Kompilationseinheiten in einer Datei bündeln, werden zunächst mit gnatchop aufgeteilt und dann mit gnatmake gebaut.

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

-gnata ist die Option, die Assertions aktiviert. Für die Nutzung von Generics selbst ist sie nicht zwingend erforderlich, macht es aber bei Lernbeispielen leichter, Verträge und Randbedingungen zu überprüfen.

Ausführbare DateignatmakegnatchopEntwicklerAusführbare DateignatmakegnatchopEntwicklerÜbergibt eine einzelne .ada-DateiAufteilung in .ads / .adb / maingnatmake -gnata mainFührt Binden und Linken durch./mainAusführungsergebnis

4. Das Grundmodell der Ada-Generics

Ada-Generics lassen sich grob anhand dieser drei Schritte verstehen.

  1. Eine generische Einheit schreiben
  2. Im generic-Teil die formalen Parameter angeben
  3. Auf der Nutzungsseite mit new instanziieren
generic-DeklarationFormale ParameterGenerischer RumpfInstanziierung mit newNutzung als gewöhnliches Unterprogramm oder PaketTypWertUnterprogrammPaket

Macht man beispielsweise das Vertauschen zweier Werte generisch, kann man allein den Typ als formalen Parameter verwenden.

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

An dieser Stelle lässt sich Generic_Swap noch nicht aufrufen. Es ist eine „Vorlage für ein Vertauschen, das für einen beliebigen Typ Element funktioniert“. Erst wenn ein konkreter Typ übergeben wird, entsteht daraus eine gewöhnliche Prozedur.

procedure Swap_Integer is new Generic_Swap (Integer);

Als Diagramm ergibt sich folgender Zusammenhang.

Integer übergebenCharacter übergebenMy_Record übergebenGeneric_Swaptype Element is privateSwap_IntegerSwap_CharacterSwap_My_RecordVertauscht Integer-VariablenVertauscht Character-VariablenVertauscht My_Record-Variablen

Wichtig ist, dass der Vorlagenrumpf ausschließlich mit den für Element verfügbaren Operationen geschrieben ist. Bei type Element is private; lassen sich Grundoperationen wie Zuweisung und Gleichheitsvergleich nutzen, Größenvergleiche oder arithmetische Operationen dagegen nicht. Die generische Deklaration selbst drückt also aus, „was diese Komponente voraussetzen darf“.

5. Arten formaler Parameter ── das Vokabular der Ada-Generics

Was Ada-Generics entgegennehmen können, sind nicht nur Typen. Das ist ein großer Unterschied zu den gängigen Generics in C# oder Java.

generic formal parametersTypparameterObjekt-/WertparameterUnterprogrammparameterPaketparametertype Element is privatetype Index is boxtype Real is digits boxMax_Size : PositiveDefault_Value : Elementwith function Less...with procedure Put ...with package P is new ...

Das box im Diagramm steht für Adas <> (den „box compound delimiter“). Da Mermaid <> nicht direkt darstellen kann, wird im Diagramm ausschließlich box geschrieben.

Die wichtigsten formalen Parameter im Überblick.

Art Beispiel Bedeutung
Typparameter type Element is private; Grundform, die einen beliebigen definiten, nicht-limitierten Typ entgegennimmt
Limitierter Typparameter type Element is limited private; Nimmt auch nicht kopierbare Typen entgegen
Diskreter Typ type Index is (<>); Typen wie Ganzzahl- oder Aufzählungstypen, die als Array-Index dienen können
Vorzeichenbehafteter Ganzzahltyp type Count is range <>; Setzt Ganzzahloperationen wie +, - und Größenvergleiche voraus
Modularer Ganzzahltyp type Word is mod <>; Für Bitoperationen und modulare Ganzzahlen
Gleitkommatyp type Real is digits <>; Float, Long_Float, benutzerdefinierte Gleitkommatypen und Ähnliches
Festkommatyp type Money is delta <>; Für Festkommaoperationen
Wertparameter Max_Size : Positive; Legt Größen oder Schwellenwerte pro Instanz fest
Unterprogramm with function Predicate (...) return Boolean; Injiziert Verhalten wie Vergleichsfunktionen oder Prädikate
Paket with package P is new Some_Generic (<>); Nimmt ein bereits instanziiertes generisches Paket als Baustein entgegen

Dank dieses Vokabulars lässt sich in Ada auf natürliche Weise nicht „nimmt alles an, macht aber intern etwas Gefährliches“ schreiben, sondern „nimmt nur Typen an, die diese Operation können“.

6. Generisches Unterprogramm ── das Minimalbeispiel Generic_Swap verstehen

Als erstes Beispiel betrachten wir Generic_Swap, das zwei Variablen eines beliebigen Typs vertauscht.

6.1 Spezifikation

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

Der auf generic folgende Teil sind die formalen Parameter. Hier wird ein Typ namens Element entgegengenommen. is private bedeutet, dass die interne Darstellung dieses Typs vom generischen Rumpf aus nicht bekannt ist.

Aus dieser Deklaration ergeben sich zwei Dinge.

  • Generic_Swap lässt sich für einen beliebigen Typ Element verwenden
  • Der Rumpf hängt weder von der internen Struktur von Element noch von einem Größenvergleich ab

6.2 Rumpf

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

Dieser Rumpf verwendet für Element ausschließlich Zuweisungen. Weder A < B noch A + B kommen vor. Deshalb funktioniert es problemlos mit Integer, Character, Record-Typen, Aufzählungstypen - kurz, mit jedem zuweisbaren Typ.

Nach dem AufrufVor dem AufrufA = 20B = 10A = 10B = 20Temp = A

6.3 Instanziierung

Auf der Nutzungsseite verwendet man new.

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

Damit lassen sich Swap_Int und Swap_Char als gewöhnliche Prozeduren aufrufen.

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;

Das Ausführungsbild sieht so aus.

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

Hier lässt sich Swap_Int (X, Y); keine Float-Variable übergeben. Swap_Int ist eine für Integer instanziierte gewöhnliche Prozedur. Generics sind leichter zu verstehen, wenn man sie nicht als „Loch, in das alles passt“ betrachtet, sondern als „Mechanismus, der pro Typ ein sicheres, konkretes Gebilde erzeugt“.

7. Generisches Paket ── Typ und Wert als Parameter

Wenn Sie nicht nur ein einzelnes Unterprogramm, sondern mehrere Operationen zusammen mit internem Zustand wiederverwenden möchten, verwenden Sie ein generisches Paket. Das klassische Beispiel ist ein Stack.

Bei einem Stack bleibt die grundlegende Logik gleich, solange sich nur Elementtyp und Maximalgröße ändern.

Generic_StackElement_TypeMax_SizePush / Pop / Size / Is_Empty / Is_FullInt_StackElement=IntegerMax_Size=5Float_StackElement=FloatMax_Size=3String_StackElement=Unbounded_StringMax_Size=20

7.1 Spezifikation

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;

Hier werden zwei Arten formaler Parameter verwendet.

  • Element_Type ist ein Typparameter
  • Max_Size ist ein Wertparameter

Da Max_Size vom Typ Positive ist, lässt sich mit einer Größe von 0 oder darunter nicht instanziieren. So können auch Wertparameter durch ihren Typ eingeschränkt werden.

7.2 Rumpf

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;

Wichtig an diesem Paketrumpf ist, dass Data und Top für jede Instanz separat angelegt werden.

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

Diese beiden entstehen aus derselben Vorlage, teilen sich aber keinen internen Zustand.

Zustand von Float_StackTopData : Float-ArrayZustand von Int_StackTopData : Integer-ArrayGeneric_StackInt_StackFloat_Stack

7.3 Zustandsübergänge des Stacks

Ein Stack lässt sich gut als Zustandsautomat betrachten.

PushPush / PopPop entnimmt das letzte ElementPush erreicht Max_SizePopPushPopEmptyNonEmptyFullOverflowUnderflow

7.4 Anwendungsbeispiel

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;

Bauen und ausführen mit demselben Vorgehen wie in Kapitel 3.

gnatchop ../src/snippets/02_stack.ada
gnatmake -gnata stack_demo
./stack_demo
Size= 3
Pop = 30
Pop = 20
Size= 1

Dass rechts von = ein Leerzeichen steht, liegt daran, dass 'Image bei Ganzzahltypen vor einem nicht-negativen Wert ein Leerzeichen einfügt. Da dreimal Push und zweimal Pop ausgeführt wurde, ergibt sich als letzter Size-Wert 1. Ruft man bei vollem Stack (in diesem Beispiel Max_Size = 5) erneut Push auf, wird Int_Stack.Stack_Overflow ausgelöst; ruft man bei leerem Stack Pop auf, wird Int_Stack.Stack_Underflow ausgelöst.

Generische Pakete bewähren sich in der Praxis häufig bei „kleinen Containern“, „Puffern fester Länge“, „Ringpuffern“, „Log-Warteschlangen“ oder „Hardware-Abstraktionsschichten“. Gerade in Ada funktioniert ein Entwurf, der die Größe statisch als Typ- oder Wertparameter festlegt, statt sie zur Laufzeit variabel zu halten, gut mit hoher Zuverlässigkeit zusammen.

8. Formaler Unterprogrammparameter ── Verhalten injizieren

Nimmt man nur den Typ entgegen, lässt sich manches noch nicht ausdrücken. Bei einer Sortierung etwa braucht man nicht nur den Elementtyp, sondern auch die Vergleichslogik, „was zuerst kommt“.

In Ada lässt sich diese Vergleichsfunktion als formaler Parameter des Generics übergeben.

Generic_Insertion_SortItem_TypeIndexItem_ArrayVergleichsfunktionStandardvergleich verwendenGreater übergeben für absteigendEigene Ordnung übergeben

8.1 Spezifikation

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);

Hier gibt es vier formale Parameter.

  1. Item_Type: der Typ des Array-Elements
  2. Index: der Typ des Array-Index
  3. Item_Array: der eigentliche Array-Typ
  4. "<": die Vergleichsfunktion

type Index is (<>); nimmt einen diskreten Typ entgegen. Das können nicht nur Ganzzahltypen, sondern auch Aufzählungstypen sein. Dass man als Array-Index nicht nur Positive, sondern auch einen Aufzählungstyp wie Day verwenden kann, ist typisch für Ada.

Das is <> in with function "<" ... is <>; bedeutet, dass beim Weglassen des aktuellen Parameters ein sichtbarer Standardoperator oder eine passende Funktion verwendet wird. Bei einem Typ wie Integer, der bereits ein < besitzt, lässt sich also auch ohne explizite Angabe einer Vergleichsfunktion arbeiten.

8.2 Rumpf

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;

Insertion Sort eignet sich nicht für große Arrays, ist aber gut geeignet, um Generics zu erklären. Weil sich nur die Vergleichsfunktion austauschen lässt, kann dieselbe Schleifenstruktur sowohl für aufsteigende als auch für absteigende Sortierung verwendet werden.

JaNeinNeinJaUnsortiertes ArrayKey von links nach rechts entnehmenSteht Key vor dem vorherigen Element?Vorheriges Element nach rechts verschiebenKey einfügenBis zum Ende bearbeitet?Sortiertes Array

8.3 Auf- und absteigende Sortierung aus demselben Rumpf erzeugen

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 verwendet das Standard-<. Sort_Desc dagegen tauscht mit "<" => Greater die Vergleichsfunktion aus.

99, 3, 47, 12Sort_AscStandardvergleichSort_DescGreater als Vergleichsfunktion3, 12, 47, 9999, 47, 12, 3

Dieser Mechanismus ähnelt dem Entwurf, in C++ ein Vergleichsfunktionsobjekt als Template-Argument zu übergeben, oder in Rust über Trait-Bounds eine Ordnung einzufordern. In Ada wird jedoch als formaler Unterprogrammparameter explizit angegeben, „eine Funktion dieser Form wird übergeben“.

8.4 Ausführungsbeispiel

Analog zum Aufbau in Kapitel 18 sieht die aufrufende Seite so aus, wenn Generic_Insertion_Sort in einer eigenen Datei (generic_insertion_sort.ads / .adb) liegt.

with Ada.Text_IO;            use Ada.Text_IO;
with Generic_Insertion_Sort;

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

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

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

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

   procedure Show (Label : String; Items : Int_Array) is
   begin
      Put (Label);
      for V of Items loop
         Put (Integer'Image (V));
      end loop;
      New_Line;
   end Show;

   Asc  : Int_Array := (99, 3, 47, 12);
   Desc : Int_Array := (99, 3, 47, 12);
begin
   Sort_Asc (Asc);
   Sort_Desc (Desc);
   Show ("Asc :", Asc);
   Show ("Desc:", Desc);
end Sort_Demo;
gnatchop ../src/snippets/03_sort.ada
gnatmake -gnata sort_demo
./sort_demo
Asc : 3 12 47 99
Desc: 99 47 12 3

Man erkennt, dass die beiden aus demselben Generic_Insertion_Sort-Rumpf erzeugten Prozeduren allein durch den Austausch der Vergleichsfunktion die umgekehrte Reihenfolge liefern. Das Leerzeichen vor jedem Element stammt daher, dass 'Image bei Ganzzahltypen vor einem nicht-negativen Wert ein Leerzeichen einfügt.

9. Typkategorien ── konkretere Verträge als private

type T is private; ist praktisch, kann aber nicht alles. Für einen private-Typ lassen sich Grundrechenarten und Größenvergleiche nicht selbstverständlich verwenden. Deshalb kann man in Ada dem formalen Typparameter eine Kategorie zuweisen.

Formal Typeprivatelimited privatediscrete box: diskreter Typrange box: vorzeichenbehaftete Ganzzahlmod box: modulare Ganzzahldigits box: Gleitkommadelta box: Festkommaaccess-TypAufzählungstypGanzzahltypFloatLong_FloatBenutzerdefinierter Gleitkommatyp

Das box im Diagramm steht für Adas <> (den „box compound delimiter“). Da Mermaid <> nicht direkt darstellen kann, wird im Diagramm ausschließlich box geschrieben.

9.1 Was bringt die Angabe einer Kategorie

Um zum Beispiel Mittelwert oder Varianz zu berechnen, braucht man Addition, Subtraktion, Multiplikation und Division. Bei einem private-Typ lassen sich diese Operationen nicht voraussetzen. Deshalb beschränkt man sich hier auf Gleitkommatypen.

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;

Durch type Real is digits <>; ist klar, dass Real ein Gleitkommatyp ist. Im generischen Rumpf lassen sich daher +, -, *, / und Ähnliches verwenden.

9.2 Rumpf

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 Verwendung mit Float und Long_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);

Dieselbe statistische Verarbeitung lässt sich für Gleitkommatypen unterschiedlicher Genauigkeit wiederverwenden.

Generic_StatisticsReal is digits boxFloat_StatsLong_StatsMy_Real_StatsMean / Variance mit FloatMean / Variance mit Long_FloatMean / Variance mit benutzerdefiniertem Real

9.4 Ausführungsbeispiel

with Ada.Text_IO;        use Ada.Text_IO;
with Ada.Float_Text_IO;  use Ada.Float_Text_IO;
with Generic_Statistics;

procedure Statistics_Demo is
   type Float_Array is array (Positive range <>) of Float;

   package Float_Stats is new Generic_Statistics (Float, Float_Array);

   Samples : constant Float_Array := (1.0, 2.0, 3.0, 4.0);
begin
   Put ("Mean     = ");
   Put (Float_Stats.Mean (Samples), Fore => 1, Aft => 3, Exp => 0);
   New_Line;

   Put ("Variance = ");
   Put (Float_Stats.Variance (Samples), Fore => 1, Aft => 3, Exp => 0);
   New_Line;
end Statistics_Demo;
gnatchop ../src/snippets/04_statistics.ada
gnatmake -gnata statistics_demo
./statistics_demo
Mean     = 2.500
Variance = 1.250

Ada.Float_Text_IO ist eine Standardbibliothek, bei der Ada.Text_IO.Float_IO für Float instanziiert wurde; gibt man Exp => 0 an, erhält man statt der Exponentialschreibweise die normale Dezimaldarstellung. Aft => 3 legt die Anzahl der Nachkommastellen fest. Beachten Sie, dass diese Variance nicht die Stichprobenvarianz, sondern die Populationsvarianz ist (Division durch Values'Length).

9.5 Die Kategorieangabe als „Spezifikation auf Typebene“

Die Kategorieangabe ist nicht bloß Syntax, um den Compiler ruhigzustellen. Sie fungiert auch als Spezifikation, die dem Leser mitteilt, „was diese Komponente voraussetzt“.

Gewünschte Operation Geeigneter formaler Typ Grund
Vertauschen, Speichern, Entnehmen private Zuweisung genügt
Verwaltung nicht kopierbarer Ressourcen limited private Setzt keine Zuweisung voraus
Array-Index, Durchlaufen von Aufzählungszuständen (<>) First, Last, Succ, Pred verfügbar
Summierung von Ganzzahlen, Zähler range <> Setzt Ganzzahlarithmetik voraus
Bitmasken, zyklische Zähler mod <> Setzt modulare Arithmetik voraus
Mittelwert, Varianz, numerische Berechnung digits <> Setzt Gleitkommaarithmetik voraus
Geldbeträge, Regelgrößen mit fester Genauigkeit delta <> Setzt Festkommaarithmetik voraus

10. Prädikat-Injektion ── Count_If auf Ada-Art schreiben

Formale Unterprogrammparameter lassen sich nicht nur für Vergleichsfunktionen, sondern auch für Prädikate verwenden. Ein Prädikat ist eine Funktion, die einen Wert entgegennimmt und einen Boolean zurückgibt.

Eine Rolle, die C#s Func<T, bool>, Javas Predicate<T> oder Lambdas und Funktionsobjekten in C++ nahekommt, lässt sich in Ada als formaler Unterprogrammparameter eines Generics ausdrücken.

10.1 Spezifikation

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;

Hier wurde Predicate bewusst kein is <> mitgegeben. Da es keine standardmäßig sichtbare Prädikatfunktion gibt, ist das Design so angelegt, dass die Nutzungsseite sie zwingend übergeben muss.

10.2 Rumpf

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;

Der Ablauf ist einfach.

TrueFalseArrayJedes Element durchlaufenPredicate(Item)?Count erhöhenNichts tunZum nächsten ElementCount zurückgeben

10.3 Zählen von geraden Zahlen und Werten über einem Schwellenwert

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);

Aus derselben Durchlauflogik lassen sich zwei Funktionen erzeugen, die sich nur in der Bedingung unterscheiden.

Generic_Count_IfCount_EvenPredicate = Is_EvenCount_LargePredicate = Is_Large12, 7, 88, 3, 56, 91, 44, 19, 62Anzahl gerader ZahlenAnzahl der Werte über 50

In diesem Beispiel sind Array-Durchlauf, Zählerverwaltung und Ergebnisrückgabe vollständig gemeinsam. „Was gezählt wird“ ist dagegen als Funktion injiziert. Das ist die Grundform höherstufigen Entwurfs in Ada.

10.4 Ausführungsbeispiel

with Ada.Text_IO;       use Ada.Text_IO;
with Generic_Count_If;

procedure Count_If_Demo is
   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);

   Data : constant Int_Array := (12, 7, 88, 3, 56, 91, 44, 19, 62);
begin
   Put_Line ("Even  =" & Natural'Image (Count_Even (Data)));
   Put_Line ("Large =" & Natural'Image (Count_Large (Data)));
end Count_If_Demo;
gnatchop ../src/snippets/05_filter.ada
gnatmake -gnata count_if_demo
./count_if_demo
Even  = 5
Large = 4

Von den 9 Elementen in Data sind 12, 88, 56, 44, 62 - also 5 - gerade, und 88, 56, 91, 62 - also 4 - größer als 50.

11. Kombination mehrerer Parameter ── ein allgemeiner Key-Value-Store

In realen Bausteinen kommt man selten mit nur einem Typparameter aus. Man muss meist mehrere Bedingungen kombinieren: den Typ von Schlüssel und Wert, die Vergleichsmethode für den Schlüssel, die Höchstzahl der Einträge.

Als Beispiel dient hier ein einfacher Key-Value-Store fester Länge.

Generic_KV_StoreKey_TypeValue_TypeSchlüsselvergleichsfunktionMax_EntriesPut / Get / ContainsKonfigurationswertspeicherKleiner CacheWörterbuch fester Länge für Embedded-Systeme

11.1 Spezifikation

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;

Dieses Paket hat vier formale Parameter.

Parameter Art Rolle
Key_Type Typ Typ des Schlüssels
Value_Type Typ Typ des Werts
"=" Unterprogramm Prüfung auf Schlüsselgleichheit
Max_Entries Wert Maximale Anzahl der Einträge

Max_Entries erhält mit := 50 einen Standardwert. Ohne explizite Angabe entsteht also ein Store mit 50 Einträgen.

11.2 Rumpf

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;

Diese Implementierung nutzt lineare Suche und ist daher nicht für große Datenmengen geeignet. Aber in Situationen, in denen feste Länge, kleine Größe und der Verzicht auf dynamische Speicherzuweisung wichtig sind, ist sie gut nutzbar.

Keys/Values/UsedGeneric_KV_Store-InstanzAufrufende SeiteKeys/Values/UsedGeneric_KV_Store-InstanzAufrufende Seitealt[Vorhandener Schlüssel][Neuer Schlüssel]Put(Key, Value)Find_Index(Key)Values(Pos) := ValueFind_FreeKeys(Pos) := KeyValues(Pos) := ValueUsed(Pos) := TrueGet(Key)Find_Index(Key)PosValues(Pos)

11.3 Instanziierungsbeispiel

with Ada.Text_IO;           use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Generic_KV_Store;

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
      Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));
   else
      Put_Line ("1 => (not found)");
   end if;

   --  Ein Put auf einen bestehenden Schlüssel überschreibt den Wert
   Int_String_Store.Put (1, To_Unbounded_String ("Ada 2022"));
   Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));

   if Int_String_Store.Contains (9) then
      Put_Line ("9 => " & To_String (Int_String_Store.Get (9)));
   else
      Put_Line ("9 => (not found)");
   end if;
end KV_Demo;
gnatchop ../src/snippets/06_kv_store.ada
gnatmake -gnata kv_demo
./kv_demo
1 => Ada
1 => Ada 2022
9 => (not found)

Ruft man Get auf, ohne vorher mit Contains die Existenz zu prüfen, wird bei fehlendem Schlüssel Key_Not_Found ausgelöst. Wählen Sie wie im obigen Beispiel eine Verzweigung über Contains oder schreiben Sie einen Ausnahmebehandler.

"=" wurde weggelassen. Integer besitzt einen Standard-Gleichheitsoperator, der durch is <> verwendet wird.

Ist der Schlüssel zum Beispiel eine Zeichenkette, bei der Groß- und Kleinschreibung ignoriert werden soll, lässt sich eine eigene Gleichheitsfunktion übergeben.

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. Formaler Paketparameter ── Generics noch weiter modularisieren

In Ada-Generics lässt sich ein ganzes Paket als formaler Parameter verwenden. Damit lässt sich „eine aus einem generischen Paket erzeugte Instanz“ als Eingabe für ein anderes Generic behandeln.

Generic_StackInt_StackGeneric_Stack_LoggerInt_Stack_Logger_Instance

12.1 Ein Logger, der einen Stack entgegennimmt

Angenommen, wir wollen einen Logger bauen, der eine Instanz des zuvor gezeigten Generic_Stack entgegennimmt und dessen Größe ausgibt.

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

Der Rumpf sieht so aus.

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;

Auf der Nutzungsseite wird zunächst der Stack erzeugt und dann dem Logger übergeben.

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);

Ein solcher Entwurf erlaubt es, generische Bausteine miteinander zu kombinieren.

2. Stufe1. StufeInt_Stack_LoggerGeneric_Stack_LoggerInt_StackGeneric_StackPrint_Size

Das ist einer Anwendung ähnlich wie C++s Template-Template-Parametern, aber in Ada lässt sich explizit angeben, „eine Instanz dieses generischen Pakets wird entgegengenommen“. In größeren Ada-Codebasen ist das nützlich, um Container, Algorithmen, Logging, Prüfung und Testhilfen getrennt zu halten und gezielt zu kombinieren.

13. Contract Model ── der wichtigste Gedanke der Ada-Generics

Für das Verständnis der Ada-Generics ist das Contract Model entscheidend.

Der generische Rumpf darf ausschließlich mit den Operationen geschrieben werden, die die formalen Parameter zusichern. Ist beispielsweise nur type Element is private; deklariert, lässt sich für Element kein < verwenden. Möchte man vergleichen, muss man dies entweder explizit als formalen Unterprogrammparameter angeben oder die Typkategorie konkreter fassen.

generic formal partVertraggeneric bodyImplementierung innerhalb des VertragsTypprüfung des Rumpfs alleinInstanziierungactual parameterstatsächliche Typen/Funktionen/WertePrüfung, ob die aktuellen Parameter den Vertrag erfüllenGewöhnliches Paket/Unterprogramm

Dieser Entwurf schützt nicht nur die Nutzer eines Generics, sondern auch diejenigen, die es schreiben.

13.1 Der Unterschied in der Erscheinung zu C++-Templates

C++-Templates sind mächtig, hatten aber historisch die Eigenschaft, dass „Fehler im Template-Rumpf erst bei der Instanziierung sichtbar wurden“. Mit den Concepts in C++20 wurde das verbessert, aber Adas Generics sind von Anfang an ein Modell, das den Vertrag explizit macht.

C++ templatesAusdrücke werden erst bei instantiation konkretisierttemplate body schreibenMit concepts lassen sich Constraints explizit machenAdabody wird innerhalb des Vertrags geprüftVertrag im formal part schreibeninstantiation prüft das actual

Bei den Generics in Java und C# stehen Referenztypen, Constraints, Type Erasure und der Bezug zur Laufzeitdarstellung im Zentrum des Entwurfs. Adas Generics dagegen orientieren sich stärker daran, zur Kompilierzeit eine konkrete Instanz zu erzeugen.

Aspekt Ada C++ Java Rust
Formulierung des Vertrags Typ, Wert, Funktion, Paket im formal part templates / concepts Typparameter und bounds trait bounds
Prüfung des Rumpfs Innerhalb des Vertrags der formalen Parameter Vor allem Konkretisierung bei der Instanziierung Innerhalb der bounds Innerhalb der trait bounds
Laufzeitkosten Grundsätzlich statisch aufgelöst Grundsätzlich statisch erzeugt Beeinflusst durch Type Erasure Grundsätzlich Monomorphisierung
Wertparameter Ja Ja Eingeschränkt const generics
Unterprogramm als formaler Parameter Ja Über Funktionsobjekte u. Ä. ausgedrückt Lambdas / funktionale Interfaces Closures/Funktionen/traits
Paket als formaler Parameter Ja Etwa Template-Template-Parameter Nicht vorhanden Getrennt von der Modulstruktur

Die einzelnen Sprachmerkmale unterscheiden sich im Detail, aber das Charakteristische an Ada ist, „den Vertrag zuerst als Syntax niederzuschreiben“.

14. Entwurfsentscheidungen in der Praxis ── was sollte generisch werden

Generics sind praktisch, aber es ist nicht sinnvoll, einfach alles generisch zu machen. In der Praxis führt folgende Entscheidungslogik seltener zu Fehlschlägen.

JaNeinJaNeinJaNeinJaNeinEs gibt Logik, die wiederverwendet werden sollUnterscheidet sich nur der Typ?Typparameter erwägenUnterscheiden sich auch Größe oder Schwellenwert?Wertparameter hinzufügenUnterscheidet sich das Vergleichs- oder Prüfverhalten?Formalen Unterprogrammparameter hinzufügenSollen interner Zustand und API gebündelt werden?Generisches PaketEin gewöhnliches Unterprogramm genügt

14.1 Wann ein generisches Unterprogramm passt

Generische Unterprogramme eignen sich für zustandslose Algorithmen.

  • Swap
  • Sort
  • Count_If
  • Find
  • Map-artige Transformationen
  • Min / Max

Ist der Algorithmusrumpf kurz und sind Eingabe und Ausgabe klar, ist ein Unterprogramm leichter lesbar als ein Paket.

14.2 Wann ein generisches Paket passt

Ein generisches Paket eignet sich, wenn Sie zusammen mit dem Typ mehrere Operationen und internen Zustand halten möchten.

  • Stack fester Länge
  • Ringpuffer
  • Kleines Wörterbuch
  • Statistik-Baustein
  • I/O-Abstraktion je Gerät
  • Rechenoperationen für Zahlentypen mit Einheitensystem

Gerade in Ada, wo die Paketspezifikation die öffentliche API und der Paketrumpf die Implementierung trennt, lässt sich ein generisches Paket als „typsichere Modulvorlage“ verwenden.

Verborgenpackage specöffentliche APINutzungsseitepackage bodyinterne Implementierunggeneric formal partVertrag über Typ, Wert, Funktion

14.3 Mit möglichst wenigen formalen Parametern beginnen

Zu viele formale Parameter machen die Instanziierung schwer lesbar. Es ist sicherer, zunächst minimal zu beginnen und erst bei konkretem Bedarf zu erweitern.

-- Beispiel, das leicht unübersichtlich wird
package X is new Generic_Foo
  (A, B, C, D, E, F, G);

-- Mit benannter Zuordnung bleibt die Absicht erkennbar
package X is new Generic_Foo
  (Element_Type => Integer,
   Index_Type   => Positive,
   Buffer_Size  => 128,
   "<"          => Less);

In Ada lässt sich bei der Instanziierung benannte Zuordnung verwenden. Da wichtige Entwurfsentscheidungen eines Generics bei der Instanziierung sichtbar werden, ist es in der Praxis meist wartungsfreundlicher, sie namentlich zu schreiben.

15. Häufige Stolperfallen

Ada-Generics sind mächtig, haben aber Punkte, an denen man zu Beginn leicht stolpert.

15.1 Bei einem private-Typ ist kein Größenvergleich möglich

Der folgende Rumpf lässt sich nicht schreiben.

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      -- Hier tritt ein Fehler auf
      return A;
   else
      return B;
   end if;
end Bad_Min;

Da Element nur als private deklariert ist, ist nicht garantiert, dass < verfügbar ist. Möchten Sie vergleichen, fügen Sie es wie folgt dem Vertrag hinzu.

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
Im Rumpf soll verglichen werdenVergleichsfunktion im formal part angebenVergleichbarkeit wird bei der Instanziierung geprüftNur privateKompilierfehler im generischen Rumpf

15.2 is <> bedeutet nicht „alles wird automatisch abgeleitet“

is <> ist praktisch, aber keine Magie. An der Instanziierungsstelle muss ein passender Operator oder ein passendes Unterprogramm sichtbar sein. Liegt eine eigene Vergleichsfunktion in einem anderen Paket, sollten Sie entweder mit with bzw. use dafür sorgen, dass sie sichtbar ist, oder sie sicherheitshalber namentlich explizit übergeben.

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

15.3 Auch Ausnahmen werden pro Instanz zu eigenständigen Objekten

Deklariert man in der Spezifikation eines generischen Pakets eine Ausnahme, wird sie für jede Instanz zu einer eigenständigen, separaten Ausnahme.

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

In diesem Fall werden Int_Stack.Stack_Overflow und Float_Stack.Stack_Overflow als verschiedene Ausnahmen behandelt. Möchten Sie sie als gemeinsame Ausnahme behandeln, sollten Sie erwägen, die Ausnahme außerhalb des Generics zu definieren.

Verschiedene AusnahmenGeneric_StackStack_Overflow-DeklarationInt_Stack.Stack_OverflowFloat_Stack.Stack_Overflow

15.4 Die Codegröße kann zunehmen

Generics vermeiden zwar leicht zusätzliche Indirektionen zur Laufzeit, erzeugen aber pro Typ eine eigene Instanz, sodass die Codegröße bei vielen Instanzen zunehmen kann.

Diesen Kompromiss kennt man auch von C++-Templates und der Monomorphisierung in Rust. In Entwicklungen mit hoher Zuverlässigkeit, Embedded- oder Echtzeitschwerpunkt bedeutet das: Man akzeptiert die Verwaltung der Artefaktgröße zur Bauzeit im Austausch gegen weniger Unsicherheit zur Laufzeit.

Ein generischer RumpfInteger-VersionFloat-VersionLong_Float-VersionMy_Type-VersionErzeugter CodeVermeidet leicht Typprüfung und Boxing zur LaufzeitBei vielen Instanzen auf Größenzuwachs achten

15.5 Wann limited private verwendet werden sollte

type Element is private; setzt Zuweisung voraus. Bei Dingen wie Dateihandles, Sperren oder Gerätehandles, die nicht kopiert werden sollen, sollten Sie limited private erwägen.

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

Für Entwürfe mit nicht kopierbaren Typen ist es sicherer, statt eines Containers, der Werte speichert, eine Prozedur zu verwenden, die eine Operation anwendet, oder ein Design, das Referenzen explizit macht.

16. Eine kleine Sammlung von Entwurfsmustern

Im Folgenden fassen wir kurz einige in der Praxis häufig verwendete Formen zusammen.

16.1 Min nur für vergleichbare Werte anbieten

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;
ElementVergleichsfunktion erforderlichGeneric_MinGibt den kleineren Wert zurück

16.2 Schwellenwert als Wertparameter

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;

Wertparameter eignen sich für Werte, die als Eigenschaft der Instanz fixiert werden sollen, statt sich zur Laufzeit als Einstellung zu ändern.

16.3 Ausgabemittel injizieren

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;

In dieser Form lässt sich das Ausgabeziel austauschen: Standardausgabe, Log, ein Testpuffer und Ähnliches.

Generic_Print_TwiceNimmt Put als formalen Unterprogrammparameter entgegenKonsolenausgabeLog-AusgabeTestpuffer

16.4 Den Index-Typ des Arrays nicht festlegen

In Ada ist auch der Index-Typ eines Arrays eine wichtige Typinformation. Statt ihn fest auf Positive zu setzen, erhöht es die Wiederverwendbarkeit, bei Bedarf auch den Index-Typ als formalen Parameter zu wählen.

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);

Mit diesem Entwurf lassen sich nicht nur Arrays mit Positive-Index verwenden, sondern auch solche mit Aufzählungstyp-Index.

Index is discretePositive rangeDay-AufzählungstypState-AufzählungstypEigener Ganzzahltyp

17. Checkliste für eine ada-typische API

Beim Schreiben von Generics lohnt es sich, am Ende noch einmal aus diesen Blickwinkeln zu prüfen - das macht sie leichter lesbar.

Prüfung des Generic-EntwurfsSind die formalen Parameter minimal?Wurden die benötigten Operationen im formal part explizit gemacht?Sind Kategorien wie private / range / digits passend gewählt?Lässt sich mit benannter Zuordnung lesbar instanziieren?Wurde an Zustand und Ausnahmen je Instanz gedacht?Ist der Zuwachs der Codegröße vertretbar?Gibt es eine Instanz für Testzwecke?

Zusammengefasst als Text:

  • Die im Rumpf verwendeten Operationen müssen immer als Vertrag in den formalen Parametern sichtbar sein.
  • Reicht private, verwenden Sie private. Ist Arithmetik nötig, verwenden Sie range <> oder digits <>.
  • Verhalten, das sich je Typ unterscheidet - Vergleich, Hashing, Ausgabe, Konvertierung -, wird zu einem formalen Unterprogramm.
  • Definiert eine Größe oder ein Schwellenwert die Eigenschaft der Instanz, machen Sie ihn zum Wertparameter.
  • Hat die Komponente Zustand, denken Sie zuerst an ein generisches Paket, ohne Zustand an ein generisches Unterprogramm.
  • Verwenden Sie bei der Instanziierung benannte Zuordnung, je mehr Argumente es gibt.
  • Entwerfen Sie so, dass Ausnahmen und interner Zustand grundsätzlich je Instanz unabhängig sind.

18. Beispielhafter Gesamtaufbau der Beispiele

Möchten Sie die Beispiele des Artikels auf Dateien verteilen, ist folgender Aufbau gut lesbar.

ada-generic-programmingsrcgenericsdemosgeneric_swap.adsgeneric_swap.adbgeneric_stack.adsgeneric_stack.adbgeneric_insertion_sort.adsgeneric_insertion_sort.adbgeneric_statistics.adsgeneric_statistics.adbgeneric_count_if.adsgeneric_count_if.adbgeneric_kv_store.adsgeneric_kv_store.adbswap_demo.adbstack_demo.adbsort_demo.adbstatistics_demo.adbcount_if_demo.adbkv_demo.adb

Für ein kleines Artikelbeispiel ist es praktisch, alles in einer Datei zu bündeln und mit gnatchop aufzuteilen. Für die Praxis und die Langzeitpflege ist es dagegen ada-typischer, Spezifikation (.ads) und Rumpf (.adb) zu trennen.

19. Zusammenfassung ── Grenzen der Wiederverwendung mit Typen festlegen

Generische Programmierung in Ada ist mehr als nur „Code schreiben, der nicht vom Typ abhängt“. Der Kern liegt vielmehr darin, auszudrücken, was eine wiederverwendbare Komponente voraussetzt - als Vertrag aus Typ, Unterprogramm und Wert.

Vertrag schreibenGenerischen Rumpf schreibenTyp, Wert, Funktion übergeben und instanziierenTypsicher verwendenOhne Kopieren wiederverwenden

Wie dieser Artikel gezeigt hat, lassen sich in Ada-Generics folgende Dinge als formale Parameter verwenden.

  • Typ
  • Wert
  • Unterprogramm
  • Paket

Für Typen lassen sich zudem recht feine Kategorien angeben: private, limited private, range <>, mod <>, digits <>, delta <>, (<>). Dadurch hängt der generische Rumpf nicht von „Operationen ab, deren Verfügbarkeit unklar ist“, sondern lässt sich sicher ausschließlich mit den im Vertrag festgelegten Operationen implementieren.

In C oder älteren C++-Beständen werden für die Wiederverwendung manchmal Makros, void*, Funktionszeiger und handgeschriebene Typverzweigungen eingesetzt. Ada-Generics können viele dieser Einsatzzwecke durch eine typsichere, lesbare Form ersetzen. Gerade bei Langzeitpflege, Embedded-Systemen, Echtzeitanforderungen und hoher Zuverlässigkeit hat dieser Entwurf, „Grenzen zur Kompilierzeit festzulegen“, großen Wert.

20. Verwandte Beratungsbereiche

Die KomuraSoft LLC übernimmt Windows-Anwendungsentwicklung, Untersuchung und Überarbeitung bestehender Software, die Abgrenzung von COM / ActiveX / 32-Bit / 64-Bit sowie technische Beratung und Design-Reviews. Neben statisch typisierten, auf hohe Zuverlässigkeit ausgerichteten Entwürfen wie in Ada ist auch die Frage, wie bestehende C/C++-, C#-, VB6-, MFC- und COM-Bestände geordnet, am Leben erhalten oder migriert werden, in der Praxis ein oft naheliegendes Thema.

  • Ada 2022 Language Reference Manual, Section 12: Generic Units ── Die eigentliche Festlegung der generischen Einheiten. 12.1 behandelt die generische Deklaration, 12.3 die Instanziierung, 12.4 die formalen Objekte (Wertparameter), 12.5 die formalen Typen (Kategorien wie private, range <>, digits <>), 12.6 die formalen Unterprogramme und 12.7 die formalen Pakete. Bei Unklarheiten in einem Kapitel dieses Artikels lohnt sich der Blick in den entsprechenden Abschnitt.
  • Ada 2022 Language Reference Manual, 2.2: Lexical Elements, Separators, and Delimiters ── Die Stelle, an der festgelegt ist, dass <> als zusammengesetztes Trennzeichen „box“ genannt wird. Die Grundlage für die box-Notation in den Diagrammen.
  • GNAT User’s Guide for Native Platforms ── Die Verwendung von gnatmake und gnatchop sowie eine Übersicht der Kompilieroptionen einschließlich -gnata. Bei Problemen mit dem Vorgehen aus Kapitel 3 hilft ein Blick hierhinein.
  • Alire Documentation ── Enthält die Installation von alr, die Verwaltung der Toolchain (GNAT / gprbuild) und die Erstellung von Crates. Für den reinen Umgebungsaufbau genügt diese Dokumentation.
  • Beispielcode (GitHub) ── Die Beispiele dieses Artikels, aufgeteilt in Dateien gemäß dem Aufbau aus Kapitel 18.

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Was sind Ada-Generics?
Ein Mechanismus zur Wiederverwendung, der Typen, Werte, Unterprogramme und sogar ganze Pakete als formale Parameter entgegennimmt und zum Zeitpunkt der Instanziierung mit `new` statisch typgeprüft wird. Es ist keine bloße Textersetzung, sondern legt bereits zur Kompilierzeit fest, ob diese Komponente den jeweiligen Vertrag erfüllt. Ein generisches Unterprogramm lässt sich allein durch seine Deklaration nicht aufrufen - erst wenn es mit einem konkreten Typ instanziiert wird, entsteht daraus eine gewöhnliche Prozedur oder Funktion.
Worin unterscheiden sich Ada-Generics von C++-Templates?
Ada setzt von Anfang an auf ein Contract Model, bei dem der Vertrag explizit gemacht wird: Der generische Rumpf darf nur Operationen verwenden, die die formalen Parameter zusichern, und wird eigenständig typgeprüft. C++-Templates hatten historisch die Eigenschaft, dass Fehler erst bei der Instanziierung sichtbar wurden - mit den Concepts in C++20 wurde das verbessert. Zudem lassen sich in Ada neben Wert- und Unterprogrammparametern auch ganze Pakete als formale Parameter verwenden, sodass sich generische Bausteine miteinander kombinieren lassen.
Warum gibt man bei einem formalen Typparameter in Ada eine Typkategorie an?
Um die im Rumpf nutzbaren Operationen als Vertrag explizit zu machen. Bei `type T is private` lassen sich nur Grundoperationen wie Zuweisung und Gleichheitsvergleich voraussetzen - Größenvergleiche oder arithmetische Operationen sind nicht verfügbar. Wird Ganzzahlarithmetik benötigt, gibt man `range <>` an, für Gleitkommaarithmetik `digits <>`, für Bitoperationen `mod <>`. Die Kategorieangabe wirkt zugleich als Spezifikation auf Typebene, die dem Leser mitteilt, was diese Komponente voraussetzt.
Worauf sollte man bei Ada-Generics achten?
Da für jeden Typ eine eigene Instanz erzeugt wird, kann die Codegröße bei vielen Instanziierungen zunehmen - denselben Kompromiss kennt man von C++-Templates und der Monomorphisierung in Rust. Außerdem wird eine im Spezifikationsteil eines generischen Pakets deklarierte Ausnahme für jede Instanz zu einer eigenen, separaten Ausnahme. In der Praxis empfiehlt es sich, mit möglichst wenigen formalen Parametern zu beginnen und erst bei konkretem Bedarf zu erweitern sowie bei Instanziierungen mit vielen Argumenten benannte Zuordnung zu verwenden.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog