Generische Programmierung in Ada ── Verträge in Typen schreiben und Wiederverwendung ohne Laufzeitkosten
· Go Komura · 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“.
flowchart LR
A[Wiederzuverwendende Logik] --> B{Wie wiederverwenden?}
B --> C[Kopieren und Einfügen]
B --> D[void* / Object / Cast]
B --> E[Ada Generics]
C --> C1[Vergessene Korrekturen wahrscheinlich]
D --> D1[Laufzeitfehler und Typverlust wahrscheinlich]
E --> E1[Typsicher]
E --> E2[Prüfung zur Kompilierzeit]
E --> E3[Kein 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.
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
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.
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]
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.
sequenceDiagram
participant Dev as Entwickler
participant Chop as gnatchop
participant Build as gnatmake
participant Exe as Ausführbare Datei
Dev->>Chop: Übergibt eine einzelne .ada-Datei
Chop-->>Dev: Aufteilung in .ads / .adb / main
Dev->>Build: gnatmake -gnata main
Build-->>Dev: Führt Binden und Linken durch
Dev->>Exe: ./main
Exe-->>Dev: Ausführungsergebnis
4. Das Grundmodell der Ada-Generics
Ada-Generics lassen sich grob anhand dieser drei Schritte verstehen.
- Eine generische Einheit schreiben
- Im
generic-Teil die formalen Parameter angeben - Auf der Nutzungsseite mit
newinstanziieren
flowchart LR
G[generic-Deklaration] --> F[Formale Parameter]
F --> B[Generischer Rumpf]
B --> I[Instanziierung mit new]
I --> U[Nutzung als gewöhnliches Unterprogramm oder Paket]
F --> F1[Typ]
F --> F2[Wert]
F --> F3[Unterprogramm]
F --> F4[Paket]
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.
flowchart TB
Template[Generic_Swap<br/>type Element is private] -->|Integer übergeben| SwapInt[Swap_Integer]
Template -->|Character übergeben| SwapChar[Swap_Character]
Template -->|My_Record übergeben| SwapRecord[Swap_My_Record]
SwapInt --> ICall[Vertauscht Integer-Variablen]
SwapChar --> CCall[Vertauscht Character-Variablen]
SwapRecord --> RCall[Vertauscht 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.
flowchart TB
P[generic formal parameters] --> T[Typparameter]
P --> O[Objekt-/Wertparameter]
P --> S[Unterprogrammparameter]
P --> PKG[Paketparameter]
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 ...]
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_Swaplässt sich für einen beliebigen TypElementverwenden- Der Rumpf hängt weder von der internen Struktur von
Elementnoch 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.
flowchart LR
subgraph Before[Vor dem Aufruf]
A1[A = 10]
B1[B = 20]
end
A1 --> T[Temp = A]
B1 --> A2[A = B]
T --> B2[B = Temp]
subgraph After[Nach dem Aufruf]
A2[A = 20]
B2[B = 10]
end
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.
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 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_Typeist ein TypparameterMax_Sizeist 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.
flowchart LR
Template[Generic_Stack] --> IntStack[Int_Stack]
Template --> FloatStack[Float_Stack]
subgraph I[Zustand von Int_Stack]
ITop[Top]
IData[Data : Integer-Array]
end
subgraph F[Zustand von Float_Stack]
FTop[Top]
FData[Data : Float-Array]
end
IntStack --> I
FloatStack --> F
7.3 Zustandsübergänge des Stacks
Ein Stack lässt sich gut als Zustandsautomat betrachten.
stateDiagram-v2
[*] --> Empty
Empty --> NonEmpty: Push
NonEmpty --> NonEmpty: Push / Pop
NonEmpty --> Empty: Pop entnimmt das letzte Element
NonEmpty --> Full: Push erreicht Max_Size
Full --> NonEmpty: Pop
Full --> Overflow: Push
Empty --> Underflow: Pop
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.
flowchart LR
A[Generic_Insertion_Sort] --> T[Item_Type]
A --> I[Index]
A --> ARR[Item_Array]
A --> CMP[Vergleichsfunktion]
CMP --> ASC[Standardvergleich verwenden]
CMP --> DESC[Greater übergeben für absteigend]
CMP --> CUSTOM[Eigene 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.
Item_Type: der Typ des Array-ElementsIndex: der Typ des Array-IndexItem_Array: der eigentliche Array-Typ"<": 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.
flowchart TB
Start[Unsortiertes Array] --> Pick[Key von links nach rechts entnehmen]
Pick --> Compare{Steht Key vor dem vorherigen Element?}
Compare -->|Ja| Shift[Vorheriges Element nach rechts verschieben]
Shift --> Compare
Compare -->|Nein| Insert[Key einfügen]
Insert --> Done{Bis zum Ende bearbeitet?}
Done -->|Nein| Pick
Done -->|Ja| End[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.
flowchart LR
Data[99, 3, 47, 12] --> A[Sort_Asc<br/>Standardvergleich]
Data --> D[Sort_Desc<br/>Greater als Vergleichsfunktion]
A --> AO[3, 12, 47, 99]
D --> DO[99, 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.
flowchart TB
FormalType[Formal Type] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete[discrete box: diskreter Typ]
FormalType --> Signed[range box: vorzeichenbehaftete Ganzzahl]
FormalType --> Modular[mod box: modulare Ganzzahl]
FormalType --> Float[digits box: Gleitkomma]
FormalType --> Fixed[delta box: Festkomma]
FormalType --> Access[access-Typ]
Discrete --> Enum[Aufzählungstyp]
Discrete --> Int[Ganzzahltyp]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[Benutzerdefinierter 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.
flowchart LR
Stats[Generic_Statistics<br/>Real is digits box] --> FS[Float_Stats]
Stats --> LS[Long_Stats]
Stats --> MS[My_Real_Stats]
FS --> FCalc[Mean / Variance mit Float]
LS --> LCalc[Mean / Variance mit Long_Float]
MS --> MCalc[Mean / 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.
flowchart LR
Arr[Array] --> Loop[Jedes Element durchlaufen]
Loop --> P{"Predicate(Item)?"}
P -->|True| Inc[Count erhöhen]
P -->|False| Skip[Nichts tun]
Inc --> Next[Zum nächsten Element]
Skip --> Next
Next --> Result[Count 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.
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[Anzahl gerader Zahlen]
L --> LR[Anzahl 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.
flowchart TB
KV[Generic_KV_Store] --> K[Key_Type]
KV --> V[Value_Type]
KV --> EQ[Schlüsselvergleichsfunktion]
KV --> M[Max_Entries]
KV --> Ops[Put / Get / Contains]
Ops --> Use1[Konfigurationswertspeicher]
Ops --> Use2[Kleiner Cache]
Ops --> Use3[Wö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.
sequenceDiagram
participant App as Aufrufende Seite
participant Store as Generic_KV_Store-Instanz
participant Data as Keys/Values/Used
App->>Store: Put(Key, Value)
Store->>Data: Find_Index(Key)
alt Vorhandener Schlüssel
Store->>Data: Values(Pos) := Value
else Neuer Schlüssel
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 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.
flowchart LR
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Int_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.
flowchart TB
subgraph Layer1[1. Stufe]
T1[Generic_Stack] --> I1[Int_Stack]
end
subgraph Layer2[2. Stufe]
T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
end
I1 --> T2
I2 --> API[Print_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.
flowchart TB
Spec[generic formal part<br/>Vertrag] --> Body[generic body<br/>Implementierung innerhalb des Vertrags]
Body --> Check1[Typprüfung des Rumpfs allein]
Spec --> Inst[Instanziierung]
Actual[actual parameters<br/>tatsächliche Typen/Funktionen/Werte] --> Inst
Inst --> Check2[Prüfung, ob die aktuellen Parameter den Vertrag erfüllen]
Check2 --> Instance[Gewö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.
flowchart LR
subgraph Ada[Ada]
A1[Vertrag im formal part schreiben] --> A2[body wird innerhalb des Vertrags geprüft]
A2 --> A3[instantiation prüft das actual]
end
subgraph CPP[C++ templates]
C1[template body schreiben] --> C2[Ausdrücke werden erst bei instantiation konkretisiert]
C2 --> C3[Mit concepts lassen sich Constraints explizit machen]
end
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.
flowchart TB
Start[Es gibt Logik, die wiederverwendet werden soll] --> Q1{Unterscheidet sich nur der Typ?}
Q1 -->|Ja| GType[Typparameter erwägen]
Q1 -->|Nein| Q2{Unterscheiden sich auch Größe oder Schwellenwert?}
Q2 -->|Ja| GObject[Wertparameter hinzufügen]
Q2 -->|Nein| Q3{Unterscheidet sich das Vergleichs- oder Prüfverhalten?}
Q3 -->|Ja| GSubp[Formalen Unterprogrammparameter hinzufügen]
Q3 -->|Nein| Q4{Sollen interner Zustand und API gebündelt werden?}
Q4 -->|Ja| GPackage[Generisches Paket]
Q4 -->|Nein| Normal[Ein gewöhnliches Unterprogramm genügt]
14.1 Wann ein generisches Unterprogramm passt
Generische Unterprogramme eignen sich für zustandslose Algorithmen.
SwapSortCount_IfFindMap-artige TransformationenMin/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.
flowchart LR
Spec[package spec<br/>öffentliche API] --> User[Nutzungsseite]
Body[package body<br/>interne Implementierung] -.Verborgen.-> User
Formal[generic formal part<br/>Vertrag über Typ, Wert, Funktion] --> Spec
Formal --> Body
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;
flowchart LR
Need[Im Rumpf soll verglichen werden] --> Contract[Vergleichsfunktion im formal part angeben]
Contract --> OK[Vergleichbarkeit wird bei der Instanziierung geprüft]
Need --> NoContract[Nur private]
NoContract --> Error[Kompilierfehler 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.
flowchart TB
Generic[Generic_Stack<br/>Stack_Overflow-Deklaration] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.Verschiedene Ausnahmen.-> F
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.
flowchart LR
Generic[Ein generischer Rumpf] --> I1[Integer-Version]
Generic --> I2[Float-Version]
Generic --> I3[Long_Float-Version]
Generic --> I4[My_Type-Version]
I1 --> Code[Erzeugter Code]
I2 --> Code
I3 --> Code
I4 --> Code
Code --> Pros[Vermeidet leicht Typprüfung und Boxing zur Laufzeit]
Code --> Cons[Bei 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;
flowchart LR
T[Element] --> C[Vergleichsfunktion erforderlich]
C --> M[Generic_Min]
M --> R[Gibt 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.
flowchart TB
Print[Generic_Print_Twice] --> Put[Nimmt Put als formalen Unterprogrammparameter entgegen]
Put --> Console[Konsolenausgabe]
Put --> Log[Log-Ausgabe]
Put --> Test[Testpuffer]
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.
flowchart LR
Index[Index is discrete] --> Positive[Positive range]
Index --> Day[Day-Aufzählungstyp]
Index --> State[State-Aufzählungstyp]
Index --> Slot[Eigener 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.
flowchart TB
C[Prüfung des Generic-Entwurfs] --> C1[Sind die formalen Parameter minimal?]
C --> C2[Wurden die benötigten Operationen im formal part explizit gemacht?]
C --> C3[Sind Kategorien wie private / range / digits passend gewählt?]
C --> C4[Lässt sich mit benannter Zuordnung lesbar instanziieren?]
C --> C5[Wurde an Zustand und Ausnahmen je Instanz gedacht?]
C --> C6[Ist der Zuwachs der Codegröße vertretbar?]
C --> C7[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 Sieprivate. Ist Arithmetik nötig, verwenden Sierange <>oderdigits <>. - 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.
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]
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.
flowchart LR
Contract[Vertrag schreiben] --> Generic[Generischen Rumpf schreiben]
Generic --> Instance[Typ, Wert, Funktion übergeben und instanziieren]
Instance --> Safe[Typsicher verwenden]
Safe --> Reuse[Ohne 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.
- Windows-Anwendungsentwicklung
- COM-Komponentenentwicklung
- Ablösung von Windows-Anwendungen
- Technische Beratung und Design-Review
- Kontakt
Referenzlinks
- 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 diebox-Notation in den Diagrammen. - GNAT User’s Guide for Native Platforms ── Die Verwendung von
gnatmakeundgnatchopsowie 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.
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Der Reiz der Sprache Ada — Design in Typen ausdrücken und Software, die jahrzehntelang läuft
Eine Einführung in den Reiz der Sprache Ada: starke Typisierung, Bereichsbeschränkungen, die Trennung von Spezifikation und Implementieru...
Echtzeitsystemprogrammierung mit Ada — Priorität, Periodizität und Ausführungszeitsteuerung in der Praxis
Ein praxisnaher Einstieg in Adas Annex D (Echtzeitsysteme) anhand von acht praktischen Codebeispielen: Task-Prioritäten, Ceiling_Locking,...
Sichere Nebenläufigkeit mit Ada — Ein praktischer Leitfaden zu Tasks und geschützten Objekten
Ein Einführungsartikel zu Adas sprachintegrierter Nebenläufigkeit mit Tasks und geschützten Objekten. Er behandelt Rendezvous (entry/acce...
Einführung in die formale Verifikation mit SPARK ── Von Ada-Verträgen zum mathematischen Beweis
Eine praxisorientierte Einführung in die formale Verifikation mit SPARK, der Ada-Teilsprache. Der Artikel zeigt den Weg von Verträgen (Pr...
Prüfung zum Registered Information Security Specialist – Frühjahr 2024 (Reiwa 6), Nachmittag, Aufgabe 1 erklärt – JWT alg=none, API-Autorisierung und vorläufige WAF-Abwehr
Anhand von Aufgabe 1 der Nachmittagsprüfung Frühjahr 2024 (Reiwa 6) zum Registered Information Security Specialist erklärt dieser Artikel...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
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.