Programmazione generic in Ada ── Contratti nei tipi e riuso a costo zero

· Aggiornato il: · · Ada, Linguaggio di Programmazione, Generics, Sistema di Tipi, Tipizzazione Statica, Contract Model, Astrazione a Costo Zero, GNAT, Alire, Alta Affidabilità, Riuso del Codice

1. Introduzione ── Non «accettare qualsiasi cosa», ma «che cosa si promette»

Quando si prova a scrivere codice riusabile in un linguaggio a tipizzazione statica, si urta subito lo stesso problema. Lo stack scritto per gli interi lo si vorrebbe usare anche con le stringhe. Sull’array in virgola mobile si vorrebbe applicare la stessa statistica. La logica di un ordinamento crescente la si vorrebbe riusare per quello decrescente. Se però si copia lo stesso codice per ogni tipo, le correzioni si dimenticano. Se al contrario si accetta qualsiasi cosa con void* o con i cast, la type safety si spezza.

La risposta di Ada sono i generic (generic units).

I generic Ada non sono una semplice sostituzione di testo. Accettano come parametri formali tipi, valori, sottoprogrammi e persino package interi, e al momento dell’istanziazione vengono type-checkati in modo statico. Non si verifica a runtime «questo tipo va davvero bene»: in compilazione si stabilisce «questo componente soddisfa il contratto».

Tre modi di riusare la stessa logicaConfronto tra copia e incolla, void-star o cast, e i generic Ada rispetto a type safety e costi a runtimeLogica da riusareCome riusarla?Copia e incollavoid* / Object / castGeneric AdaFacile dimenticare una correzioneErrori a runtime e perdita di type safetyType-safeControllo in compilazioneNessun dispatch extra a runtime

In questo articolo organizziamo la programmazione generic in Ada nel seguente ordine.

  • Sottoprogrammi generici
  • Package generici
  • Parametri di tipo, di valore e di sottoprogramma
  • Categorie di tipo come private, range <>, digits <>
  • Esempi di implementazione: ordinamento, stack, statistica, Count_If, key-value store
  • Generic di ordine superiore tramite parametri formali di package
  • Il contract model di Ada e i criteri di progettazione pratica

1.1 A chi è rivolto e che cosa potete portarvi via

L’articolo è pensato per chi si riconosce in uno di questi profili.

  • Chi ha già usato template C++, generics C# o Java, oppure i generic di Rust
  • Chi non è esperto della sintassi Ada ma è interessato a una progettazione in cui «i contratti si scrivono nei tipi»
  • Chi, in manutenzione di lungo periodo, embedded o alta affidabilità, sta valutando come progettare componenti riusabili

Come sintassi Ada basta conoscere i package (separazione tra specifica .ads e corpo .adb) e i modi dei parametri in / out / in out. Se questi due punti sono incerti, leggere prima «Il fascino del linguaggio Ada» accelera la comprensione.

Ciò che potete portarvi via non è tanto la sintassi Ada in sé, quanto l’abitudine di progettare: scrivere come tipi, prima dell’implementazione, che cosa un componente riusabile richiede. Lo stesso ragionamento si applica quando decidete fino a dove spingere i vincoli di interfaccia in C# o i concepts di C++20.

1.2 Guida alla lettura ── Non serve leggere tutto

L’articolo ha 20 capitoli. Non è necessario leggerli in sequenza: potete selezionare in base allo scopo.

Scopo Capitoli da leggere
Capire in poco tempo solo l’idea Cap. 4 (modello di base) → cap. 6 (sottoprogramma generico minimo) → cap. 13 (contract model)
Scrivere i componenti da soli Cap. 4 → cap. 6 → cap. 7 (package generico) → cap. 8 (iniezione di comportamento) → cap. 9 (categorie di tipo)
Solo i criteri di decisione progettuale Cap. 13 → cap. 14 (che cosa rendere generic) → cap. 15 (punti in cui si inciampa) → cap. 17 (checklist)
Mettere le mani sul codice Preparare l’ambiente nel cap. 3 e partire dai campioni completi dei cap. 6 e 7

Il percorso più breve sono i tre capitoli 4, 6 e 13. In questi tre avete il nucleo: parametri formali, istanziazione, contract model. I capitoli 5 e 9 sono elenchi di parametri formali: va bene consultarli come dizionario quando servono.

Il tema si colloca nella serie del blog dopo «Il fascino del linguaggio Ada», «Introduzione alla verifica formale con SPARK», «Concorrenza sicura in Ada» e «Sistemi real-time in Ada». Qui approfondiamo, dal lato dei generic, l’idea Ada di «far parlare i tipi del progetto».

Mappa della conoscenza di questo articolo

I generic Ada sono un meccanismo di riuso che accetta tipi, valori, sottoprogrammi e package come parametri formali e che, al momento dell’istanziazione con new, viene type-checkato in modo statico. I sottoprogrammi generici e i package generici sono i due pilastri di questo meccanismo: i parametri di tipo formali rendono esplicito, tramite categorie come private, range <> e digits <>, il contratto delle operazioni che il corpo può usare; i parametri formali di sottoprogramma iniettano comportamenti come confronti e predicati; i parametri formali di package accettano come componente un package generico già istanziato. Grazie al contract model, che type-checka il corpo da solo nel solo raggio dei parametri formali, il problema che i template C++ hanno avuto storicamente, per cui l’errore si vede solo all’istanziazione, in Ada non si verifica per costruzione. Con GNAT e Alire si può provare questo meccanismo in un ambiente gratuito.

Mappa della conoscenza della programmazione generic in AdaDiagramma che mostra come i sottoprogrammi generici e i package generici poggino sul contratto (contract model) dei parametri formali di tipo, valore, sottoprogramma e package, come l'istanziazione con new ne verifichi il contratto, e la differenza rispetto al problema storico dei template C++ per cui gli errori emergevano solo all'istanziazioneimplementausausarichiedeusausarichiedeconfigurato darichiederichiedeimplementaimplementaprevienepuò causarerichiedeimplementaconfigurato dausausarichiedeimplementaGeneric di Ada (unità generiche)Contract model dei generic AdaAda (linguaggio di programmazione)Sottoprogramma generico (Generic Subprogram)Package generico (Generic Package)Parametro formale di tipo (Formal Type)Parametro formale di sottoprogramma (Formal Subprogram)Parametro formale di package (Formal Package)Istanziazione (instantiation)Parametro formale di oggetto (valore) (Formal Object)Errore di template rilevabile solo all'istanziazioneTemplate C++GNATAlire

Nel diagramma, una linea continua indica una relazione che vale sempre e una linea tratteggiata indica una relazione condizionale (le condizioni sono nella spiegazione di ciascuna relazione nella pagina di dettaglio). L’elenco completo delle relazioni (in totale 21, con evidenza e livello di certezza) e le definizioni dei concetti principali sono raccolti nella pagina di dettaglio della mappa della conoscenza (in giapponese). Dati: JSON-LD / Turtle

2. La mappa di questo articolo

Prima fissiamo il quadro d’insieme. Se i generic Ada li si capisce solo come «una funzione che prende un tipo come argomento», la visione è stretta. In realtà, a seconda dell’unità che si vuole riusare, si combinano sottoprogrammi, package, parametri di sottoprogramma, parametri valore e parametri formali di package.

Ada GenericsSottoprogramma genericoSwapCount_IfSortPackage genericoStackStatisticsKV StoreParametri formaliTipoprivatelimited privaterange boxmod boxdigits boxdelta boxdiscrete boxOggettoMax_SizeThresholdSottoprogrammaFunzione LessFunzione EqualsPredicatePackagewith package P is newGenericIdee di progettazioneContract ModelControllo staticoAstrazione a costo zeroSpecifica e corpo separati

Nei diagrammi box indica il <> di Ada (box compound delimiter). Mermaid non riesce a disegnare <> così com’è, quindi solo nei diagrammi si scrive box.

Il modo di leggere l’articolo è semplice. Nella prima metà seguiamo la sintassi, nella seconda le decisioni di progettazione. Se è la prima volta che leggete Ada, all’inizio non cercate di memorizzare i dettagli sintattici: concentratevi su che cosa è stato reso parametro formale e quali operazioni quel parametro consente.

2.1 Mini-glossario

Prima di proseguire, mettiamo in corrispondenza i termini che torneranno più volte. Nella letteratura Ada, generic si rende spesso con «generico»; in questo articolo useremo «sottoprogramma generico» e «package generico», con lo stesso significato di «generic».

Notazione in questo articolo Inglese Significato
Unità generica generic unit Dichiarazione che inizia con generic. Nome collettivo per sottoprogrammi generici e package generici
Parametro formale generic formal parameter Argomento lato ricevente, scritto tra generic e il corpo della dichiarazione. Tipo, valore, sottoprogramma o package
formal part generic formal part La parte in cui si elencano i parametri formali. Si può leggere come «il posto in cui si scrive il contratto»
Parametro attuale generic actual parameter Tipo, valore, sottoprogramma o package effettivamente passati all’istanziazione
Istanziazione instantiation Creare, con new, un sottoprogramma o un package ordinario a partire da un’unità generica
box <> Nome del simbolo <> in Ada. In range <> indica che il tipo concreto si decide all’istanziazione
contract model contract model Il modo Ada di scrivere il corpo solo nel raggio delle promesse dei parametri formali e di type-checkare il corpo da solo (cap. 13)

Nel testo scriviamo range <>, digits <> come nella notazione Ada, ma solo nei diagrammi scriviamo range box, digits box. Mermaid non disegna etichette che contengono <>; il significato è lo stesso del testo. Come nella tabella, Ada stesso chiama <> box, quindi la notazione dei diagrammi non esce dal vocabolario Ada.

3. Ambiente di esecuzione e compilazione

Il codice di questo articolo presuppone GNAT 15.x o successivo. GNAT è il compilatore Ada di riferimento e si può installare da Alire. Alire è il package manager di Ada / SPARK e serve anche a gestire toolchain e build.

gnat --version
# GNAT 15.2.1

Installate GNAT da Alire (il package manager Ada) con alr install gnat_native gprbuild e mettete i binari nel PATH.

I campioni trattati qui si intendono disposti così nel repository.

Disposizione dei campioni nel repositoryI frammenti 01_swap.ada fino a 06_kv_store.ada stanno in src/snippets sotto ada-generic-programmingada-generic-programming/src/snippets/01_swap.ada02_stack.ada03_sort.ada04_statistics.ada05_filter.ada06_kv_store.adaREADME.md

I campioni che riuniscono più unità di compilazione in un solo file si spezzano con gnatchop e poi si compilano con gnatmake.

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

-gnata abilita le asserzioni. Non è obbligatorio per usare i generic in sé, ma nei campioni didattici rende più facile verificare contratti e condizioni di bordo.

Flusso da un file .ada all'eseguibilegnatchop spezza il file, gnatmake fa bind e link, poi si esegue il programmaEseguibilegnatmakegnatchopSviluppatoreEseguibilegnatmakegnatchopSviluppatorePassa un solo file .adaSpezza in .ads / .adb / maingnatmake -gnata mainEsegue bind e link./mainOutput

4. Il modello di base dei generic Ada

I generic Ada si capiscono bene, in larga sintesi, in tre passi.

  1. Si scrive un’unità generica
  2. Nella parte generic si scrivono i parametri formali
  3. Dal lato utente si istanzia con new
Tre passi del modello generic AdaDalla dichiarazione generic ai parametri formali, al corpo, all'istanziazione con new e all'uso come sottoprogramma o package ordinariodichiarazione genericParametri formaliCorpo del genericIstanziazione con newUso come sottoprogramma o package ordinarioTipiValoriSottoprogrammiPackage

Per esempio, se si rende generic lo scambio di due valori, basta il tipo come parametro formale.

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

A questo punto Generic_Swap non è ancora chiamabile. È «il template di uno scambio utilizzabile per un tipo Element qualsiasi». Diventa una procedura ordinaria solo quando si fornisce un tipo concreto.

procedure Swap_Integer is new Generic_Swap (Integer);

In figura, la relazione è questa.

Istanziazione di Generic_Swap per tipoLo stesso template produce Swap_Integer, Swap_Character e Swap_My_Record a seconda del tipo passatoPassa IntegerPassa CharacterPassa My_RecordGeneric_Swaptype Element is privateSwap_IntegerSwap_CharacterSwap_My_RecordScambia variabili IntegerScambia variabili CharacterScambia variabili My_Record

Il punto cruciale è che il corpo del template è scritto solo con le operazioni disponibili su Element. Se si dichiara type Element is private;, si possono usare operazioni di base come assegnamento e uguaglianza, ma non confronti di ordine né aritmetica. In altre parole, la dichiarazione generic stessa dice «che cosa questo componente può presupporre».

5. Tipi di parametro formale ── Il vocabolario dei generic Ada

Ciò che un generic Ada può ricevere non è solo un tipo. È qui che si discosta in modo netto dai generics tipici di C# o Java.

Quattro tipi di parametro formaleI parametri formali Ada sono tipi, oggetti o valori, sottoprogrammi e packagegeneric formal parametersParametri di tipoParametri oggetto / valoreParametri di sottoprogrammaParametri di packagetype Element is privatetype Index is boxtype Real is digits boxMax_Size : PositiveDefault_Value : Elementwith function Less...with procedure Put ...with package P is new ...

Nei diagrammi box indica il <> di Ada (box compound delimiter). Mermaid non riesce a disegnare <> così com’è, quindi solo nei diagrammi si scrive box.

I parametri formali rappresentativi, in tabella, sono i seguenti.

Tipo Esempio Significato
Parametro di tipo type Element is private; Forma di base: accetta un tipo definite non limited qualsiasi
Parametro limited type Element is limited private; Accetta anche tipi che non si possono copiare
Tipo discreto type Index is (<>); Tipi usabili come indici di array, come interi o enumerazioni
Intero con segno type Count is range <>; Si possono presupporre +, -, confronti e l’aritmetica intera
Intero modulare type Word is mod <>; Operazioni bit a bit e interi con aritmetica modulare
Virgola mobile type Real is digits <>; Float, Long_Float, tipi floating-point definiti dall’utente
Virgola fissa type Money is delta <>; Aritmetica in virgola fissa
Parametro valore Max_Size : Positive; Fissa per istanza dimensioni, soglie e simili
Sottoprogramma with function Predicate (...) return Boolean; Inietta un comportamento: confronto, predicato e così via
Package with package P is new Some_Generic (<>); Accetta come componente un package generico già istanziato

Grazie a questo vocabolario, in Ada è naturale scrivere non «accetto qualsiasi cosa e dentro faccio operazioni pericolose», ma «accetto solo i tipi su cui queste operazioni sono possibili».

6. Sottoprogrammi generici ── Capire la forma minima con Generic_Swap

Come primo esempio, vediamo Generic_Swap, che scambia due variabili di un tipo qualsiasi.

6.1 Specifica

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

La parte che segue generic è l’elenco dei parametri formali. Qui si riceve un tipo chiamato Element. is private significa che, dal punto di vista del corpo del generic, la rappresentazione interna di quel tipo non è nota.

Da questa dichiarazione risultano chiare due cose.

  • Generic_Swap è utilizzabile per un tipo Element qualsiasi
  • Il corpo non dipende dalla struttura interna di Element né da confronti di ordine

6.2 Corpo

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

In questo corpo, su Element si usa solo l’assegnamento. Non c’è A < BA + B. Quindi funziona in modo naturale per Integer, Character, tipi record, enumerazioni: qualunque tipo assegnabile.

Scambio di due valori tramite TempPrima della chiamata A vale 10 e B vale 20; dopo, tramite Temp, A vale 20 e B vale 10Dopo la chiamataPrima della chiamataA = 20B = 10A = 10B = 20Temp = A

6.3 Istanziazione

Dal lato utente si usa new.

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

Così Swap_Int e Swap_Char sono chiamabili come procedure ordinarie.

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;

L’output è il seguente.

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

A Swap_Int (X, Y); non si può passare una variabile Float. Swap_Int è una procedura ordinaria istanziata per Integer. I generic non sono «un buco in cui entra qualsiasi cosa»: sono «un meccanismo che produce un oggetto concreto type-safe per ciascun tipo».

7. Package generici ── Parametrizzare insieme tipo e valore

Quando non basta un solo sottoprogramma e si vogliono riusare più operazioni insieme a uno stato interno, si usa un package generico. L’esempio classico è lo stack.

Nello stack, se si cambiano solo il tipo degli elementi e la capacità massima, la logica di base resta la stessa.

Generic_Stack parametrizzato per tipo e capacitàGeneric_Stack riceve Element_Type e Max_Size e produce istanze Int_Stack, Float_Stack e String_StackGeneric_StackElement_TypeMax_SizePush / Pop / Size / Is_Empty / Is_FullInt_StackElement=IntegerMax_Size=5Float_StackElement=FloatMax_Size=3String_StackElement=Unbounded_StringMax_Size=20

7.1 Specifica

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;

Qui si usano due tipi di parametro formale.

  • Element_Type è un parametro di tipo
  • Max_Size è un parametro valore

Max_Size è Positive, quindi non si può istanziare con una capacità minore o uguale a zero. Anche i parametri valore portano vincoli di tipo.

7.2 Corpo

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;

Nel corpo del package, il punto importante è che Data e Top vengono creati separatamente per ciascuna istanza.

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

Le due istanze nascono dallo stesso template, ma non condividono lo stato interno.

Stato indipendente per ciascuna istanza di stackInt_Stack e Float_Stack hanno ciascuno il proprio Top e il proprio array DataStato di Float_StackTopData : array di FloatStato di Int_StackTopData : array di IntegerGeneric_StackInt_StackFloat_Stack

7.3 Transizioni di stato dello stack

Lo stack si capisce bene se lo si guarda come macchina a stati.

Macchina a stati di uno stack a capacità fissaPush e Pop spostano lo stack tra vuoto, non vuoto e pieno, e Push/Pop illegali portano a overflow o underflowPushPush / PopPop estrae ultimo elementoPush raggiunge Max_SizePopPushPopEmptyNonEmptyFullOverflowUnderflow

7.4 Esempio d’uso

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;

Si compilano ed eseguono con la stessa procedura del capitolo 3.

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

Lo spazio dopo = è dovuto alla specifica di 'Image sui tipi interi, che mette un carattere di spazio davanti a un valore non negativo. Dopo tre Push e due Pop, l’ultimo Size è 1. Se si chiama Push a stack pieno (in questo esempio 5 elementi, Max_Size) viene sollevata Int_Stack.Stack_Overflow; se si chiama Pop a stack vuoto, Int_Stack.Stack_Underflow.

I package generici, in pratica, funzionano bene per «piccoli contenitori», «buffer a lunghezza fissa», «ring buffer», «code di log», «strati di astrazione hardware». In Ada, in particolare, fissare le dimensioni in modo statico come parametri di tipo o di valore, invece di muoverle a runtime, sta bene insieme alla progettazione di sistemi ad alta affidabilità.

8. Parametri formali di sottoprogramma ── Iniettare un comportamento

Anche ricevendo solo un tipo, resta qualcosa che non si riesce a esprimere. In un ordinamento, per esempio, serve non solo il tipo degli elementi ma anche la logica di confronto: quale elemento viene prima.

In Ada, questa funzione di confronto può essere un parametro formale del generic.

Generic_Insertion_Sort e la funzione di confrontoOltre a Item_Type, Index e Item_Array, il confronto si può sostituire con l'ordine standard, Greater o un ordine personalizzatoGeneric_Insertion_SortItem_TypeIndexItem_ArrayFunzione di confrontoUsa il confronto standardPassa Greater per l'ordine decrescentePassa un ordine personalizzato

8.1 Specifica

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

Ci sono quattro parametri formali.

  1. Item_Type: tipo degli elementi dell’array
  2. Index: tipo degli indici dell’array
  3. Item_Array: il tipo array effettivo
  4. "<": la funzione di confronto

type Index is (<>); accetta un tipo discreto. Non solo interi: anche enumerazioni. Potere usare come indice non solo Positive ma un’enumerazione come Day è un tratto tipicamente Ada.

In with function "<" ... is <>;, is <> significa che, se si omette il parametro attuale, si usa l’operatore standard visibile o una funzione compatibile. Quindi, per un tipo che ha già < come Integer, si può usare il generic senza indicare esplicitamente la funzione di confronto.

8.2 Corpo

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;

L’insertion sort non è adatto ad array grandi, ma è adatto a spiegare i generic. Sostituendo solo la funzione di confronto, la stessa struttura di ciclo serve sia per l’ordine crescente sia per quello decrescente.

Flusso dell'insertion sortSi prende Key da sinistra, si confronta con l'elemento precedente, lo si sposta a destra se necessario e si inserisce Key finché l'array è ordinatoYesNoNoYesArray non ordinatoPrendi Key da sinistraKey precede l'elemento precedente?Sposta a destra l'elemento precedenteInserisci KeyElaborazione finita?Array ordinato

8.3 Ottenere crescente e decrescente dallo stesso corpo

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 usa il < standard. Sort_Desc, invece, sostituisce la funzione di confronto con "<" => Greater.

Crescente e decrescente dallo stesso corpoSort_Asc con il confronto standard e Sort_Desc con Greater producono ordini opposti sugli stessi dati99, 3, 47, 12Sort_Ascconfronto standardSort_DescGreater passato come confronto3, 12, 47, 9999, 47, 12, 3

Il meccanismo è vicino al passare un function object come argomento di template in C++, o al richiedere un ordinamento con un trait bound in Rust. In Ada, però, si rende esplicito come parametro formale di sottoprogramma: «passa una funzione di questa forma».

8.4 Esempio di esecuzione

Seguendo la struttura del capitolo 18, se Generic_Insertion_Sort sta in file separati (generic_insertion_sort.ads / .adb), il lato chiamante è il seguente.

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

Si verifica che due procedure nate dallo stesso corpo di Generic_Insertion_Sort risultino in ordine inverso solo sostituendo la funzione di confronto. Lo spazio davanti a ciascun elemento è quello che 'Image degli interi mette prima di un valore non negativo.

9. Categorie di tipo ── Scrivere un contratto più concreto di private

type T is private; è comodo, ma non permette tutto. Su un tipo private non si possono dare per scontate le quattro operazioni aritmetiche né i confronti di ordine. Per questo Ada consente di specificare una categoria sul parametro di tipo formale.

Categorie di tipo dei parametri formaliprivate, limited private, discreto, intero con segno, modulare, virgola mobile, virgola fissa e accessFormal Typeprivatelimited privatediscrete box: tipo discretorange box: intero con segnomod box: intero modularedigits box: virgola mobiledelta box: virgola fissatipo accessEnumerazioneTipo interoFloatLong_FloatFloating-point definito dall'utente

Nei diagrammi box indica il <> di Ada (box compound delimiter). Mermaid non riesce a disegnare <> così com’è, quindi solo nei diagrammi si scrive box.

9.1 Che cosa si guadagna specificando una categoria

Per calcolare media e varianza servono addizione, sottrazione, moltiplicazione e divisione. Un tipo private non può presupporre queste operazioni. Quindi si restringe ai tipi in virgola mobile.

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;

Con type Real is digits <>; si capisce che Real è un tipo in virgola mobile. Di conseguenza, nel corpo del generic si possono usare +, -, *, /.

9.2 Corpo

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 Uso con Float e 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);

La stessa statistica si può riusare su tipi in virgola mobile di precisione diversa.

Stessa statistica, tre precisioniGeneric_Statistics si istanzia per Float, Long_Float e un Real definito dall'utenteGeneric_StatisticsReal is digits boxFloat_StatsLong_StatsMy_Real_StatsMean / Variance su FloatMean / Variance su Long_FloatMean / Variance su Real utente

9.4 Esempio di esecuzione

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 è la libreria standard che istanzia Ada.Text_IO.Float_IO per Float; con Exp => 0 si ottiene la notazione decimale ordinaria, non quella esponenziale. Aft => 3 è il numero di cifre dopo la virgola. Attenzione: questa Variance è la varianza della popolazione (si divide per Values'Length), non la varianza campionaria.

9.5 La categoria è una «specifica a livello di tipo»

Specificare una categoria non è solo sintassi per far stare zitto il compilatore. Per il lettore è anche una specifica che comunica «che cosa questo componente richiede».

Elaborazione che si vuole scrivere Tipo formale adatto Perché
Scambio, memorizzazione, prelievo private Basta l’assegnamento
Gestione di risorse non copiabili limited private Non si presuppone l’assegnamento
Indici di array, scansione di stati enumerati (<>) Si possono usare First, Last, Succ, Pred
Somma di interi, contatori range <> Si può presupporre l’aritmetica intera
Maschere di bit, contatori circolari mod <> Si può presupporre l’aritmetica modulare
Media, varianza, calcolo numerico digits <> Si può presupporre l’aritmetica in virgola mobile
Importi, grandezze di controllo a precisione fissa delta <> Si può presupporre l’aritmetica in virgola fissa

10. Iniezione di predicati ── Scrivere Count_If in modo Ada

I parametri formali di sottoprogramma non servono solo alle funzioni di confronto: si usano anche sui predicati. Un predicato è una funzione che riceve un valore e restituisce Boolean.

Il ruolo vicino a Func<T, bool> in C#, Predicate<T> in Java, lambda o function object in C++, in Ada si esprime come sottoprogramma formale di un generic.

10.1 Specifica

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;

Qui a Predicate non abbiamo messo is <>. Non esiste una funzione predicato standard visibile, quindi il progetto chiede al chiamante di passarla sempre.

10.2 Corpo

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;

Il flusso è semplice.

Flusso di Generic_Count_IfSi scorre l'array, si incrementa Count se Predicate è vero e alla fine si restituisce CountTrueFalseArrayScorri gli elementiPredicate(Item)?Incrementa CountNon fare nullaElemento successivoRestituisci Count

10.3 Conteggio dei pari e conteggio sopra soglia

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

Dalla stessa logica di scansione si ottengono due funzioni che differiscono solo per la condizione.

Due conteggi dallo stesso Generic_Count_IfCount_Even con Is_Even e Count_Large con Is_Large applicati allo stesso arrayGeneric_Count_IfCount_EvenPredicate = Is_EvenCount_LargePredicate = Is_Large12, 7, 88, 3, 56, 91, 44, 19, 62Numero di pariNumero maggiori di 50

In questo esempio, scansione dell’array, gestione del contatore e restituzione del risultato sono tutti in comune. Si inietta come funzione solo che cosa contare. Questa è la forma di base della progettazione di ordine superiore in Ada.

10.4 Esempio di esecuzione

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

Tra i 9 elementi di Data, i pari sono 12, 88, 56, 44, 62 (5) e quelli maggiori di 50 sono 88, 56, 91, 62 (4).

11. Composizione di più parametri ── Un key-value store generico

Nei componenti reali, un solo parametro di tipo non basta quasi mai. Tipo della chiave, tipo del valore, modo di confrontare le chiavi, numero massimo di voci: si combinano più condizioni.

Qui prendiamo come esempio un key-value store semplice a lunghezza fissa.

Parametri e usi di Generic_KV_StoreGeneric_KV_Store combina Key_Type, Value_Type, uguaglianza delle chiavi e Max_Entries, con Put, Get e ContainsGeneric_KV_StoreKey_TypeValue_TypeFunzione di uguaglianza delle chiaviMax_EntriesPut / Get / ContainsStore di valori di configurazioneCache di piccole dimensioniDizionario a lunghezza fissa per embedded

11.1 Specifica

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;

Questo package ha quattro parametri formali.

Parametro Tipo Ruolo
Key_Type Tipo Tipo della chiave
Value_Type Tipo Tipo del valore
"=" Sottoprogramma Test di uguaglianza delle chiavi
Max_Entries Valore Numero massimo di entry

A Max_Entries diamo il default := 50. Se non si specifica, lo store ha 50 posti.

11.2 Corpo

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;

L’implementazione è una ricerca lineare, quindi non è per grandi volumi di dati. È però una forma comoda quando contano lunghezza fissa, piccola scala e assenza di allocazione dinamica.

Put e Get in Generic_KV_StorePut aggiorna o inserisce una chiave; Get cerca l'indice e restituisce Values(Pos)Keys/Values/UsedIstanza di Generic_KV_StoreChiamanteKeys/Values/UsedIstanza di Generic_KV_StoreChiamantealt[Chiave già presente][Nuova chiave]Put(Key, Value)Find_Index(Key)Values(Pos) := ValueFind_FreeKeys(Pos) := KeyValues(Pos) := ValueUsed(Pos) := TrueGet(Key)Find_Index(Key)PosValues(Pos)

11.3 Esempio di istanziazione

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;

   --  Un Put su una chiave esistente sovrascrive il valore
   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)

Se si chiama Get senza aver verificato l’esistenza con Contains, in assenza della chiave viene sollevata Key_Not_Found. Scegliete se ramificare con Contains, come nell’esempio sopra, o scrivere un gestore di eccezioni.

"=" è omesso. Integer ha l’operatore di uguaglianza standard e is <> lo usa.

Se la chiave fosse una stringa che ignora maiuscole e minuscole, si può passare una funzione di uguaglianza propria.

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. Parametri formali di package ── Rendere i generic ulteriormente componibili

Nei generic Ada, un package intero può essere un parametro formale. Così si può trattare «un’istanza nata da un certo package generico» come input di un altro generic.

Concatenare Generic_Stack e un loggerInt_Stack nasce da Generic_Stack e viene passato a Generic_Stack_LoggerGeneric_StackInt_StackGeneric_Stack_LoggerInt_Stack_Logger_Instance

12.1 Un logger che riceve uno stack

Supponiamo di voler costruire un logger che riceve un’istanza del Generic_Stack di prima e ne mostra la dimensione.

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

Il corpo è il seguente.

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;

Dal lato utente si crea prima lo stack e poi lo si passa al logger.

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

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

Con questo progetto si possono combinare componenti generic tra loro.

Due stadi di istanziazionePrima si istanzia Generic_Stack in Int_Stack, poi si passa Int_Stack a Generic_Stack_LoggerSecondo stadioPrimo stadioInt_Stack_LoggerGeneric_Stack_LoggerInt_StackGeneric_StackPrint_Size

L’uso è vicino ai template template parameter del C++, ma in Ada si può dire esplicitamente «ricevi un’istanza di questo package generico». Nel codice Ada di grandi dimensioni è comodo quando si vogliono tenere separati e poi combinare contenitori, algoritmi, log, controlli, ausili di test.

13. Contract Model ── L’idea più importante dei generic Ada

Per capire i generic Ada, il punto centrale è il contract model.

Il corpo del generic deve essere scritto usando solo le operazioni promesse dai parametri formali. Per esempio, se si è dichiarato solo type Element is private;, non si può usare < su Element. Se si vuole <, o lo si rende esplicito come sottoprogramma formale, o si rende più concreta la categoria di tipo.

Contract model dei generic AdaIl corpo si type-checka da solo nel raggio del formal part; all'istanziazione si verifica che i parametri attuali soddisfino il contrattogeneric formal partcontrattogeneric bodyimplementazione nel raggio del contrattoType-check del corpo da soloIstanziazioneactual parameterstipo, funzione e valore realiI parametri attuali soddisfano il contratto?Package / sottoprogramma ordinario

Grazie a questo progetto, è protetto non solo chi usa il generic ma anche chi lo scrive.

13.1 Come appare diverso rispetto ai template C++

I template C++ sono potenti, ma storicamente avevano la proprietà che «l’errore esce solo quando si istanzia il corpo del template». C++20 con i concepts ha migliorato la situazione; i generic Ada, però, sono un modello in cui il contratto è esplicito fin dall’inizio.

Ada e i template C++ a confrontoAda scrive il contratto nel formal part e controlla il corpo; i template C++ concretizzano le richieste all'istanziazione, con i concepts come vincolo esplicitoC++ templatesAll'instantiation si concretizzano le richiesteSi scrive il template bodyCon i concepts i vincoli si possono rendere esplicitiAdaIl body si controlla nel contrattoSi scrive il contratto nel formal partAll'instantiation si controllano gli actual

I generics di Java e C# ruotano intorno a tipi riferimento, vincoli, type erasure e rappresentazione a runtime. I generic Ada, invece, stanno più vicini all’idea di produrre un’istanza concreta in compilazione.

Aspetto Ada C++ Java Rust
Come si scrive il contratto Nel formal part si scrivono tipo, valore, funzione, package templates / concepts Parametri di tipo e bounds trait bounds
Controllo del corpo Nel raggio del contratto dei parametri formali Centralmente la concretizzazione all’istanziazione Nel raggio dei bounds Nel raggio dei trait bounds
Costo a runtime Risoluzione statica di base Generazione statica di base Influenzato dal type erasure Monomorfizzazione di base
Parametri valore Limitati const generics
Sottoprogramma come parametro formale Function object e simili Lambda / interfacce funzionali Closure / funzione / trait
Package come parametro formale Template template e simili No Distinto dalla struttura dei moduli

Le funzionalità di dettaglio differiscono, ma il tratto Ada è «scrivere prima il contratto come sintassi».

14. Decisioni di progettazione in pratica ── Che cosa rendere generic

I generic sono comodi, ma non va bene rendere generic qualsiasi cosa. In pratica si sbaglia meno ragionando così.

Albero di decisione su che cosa rendere genericSi valutano in ordine tipo, dimensione o soglia, comportamento di confronto, stato interno; altrimenti basta un sottoprogramma ordinarioYesNoYesNoYesNoYesNoC'è un'elaborazione da riusareCambia solo il tipo?Valuta un parametro di tipoCambiano anche dimensione o soglia?Aggiungi un parametro valoreCambia il comportamento di confronto o giudizio?Aggiungi un sottoprogramma formaleVuoi raggruppare stato interno e API?Package genericoBasta un sottoprogramma ordinario

14.1 Quando è adatto un sottoprogramma generico

I sottoprogrammi generici stanno bene agli algoritmi senza stato.

  • Swap
  • Sort
  • Count_If
  • Find
  • Trasformazioni in stile Map
  • Min / Max

Quando il corpo dell’algoritmo è corto e input e output sono chiari, un sottoprogramma è più leggibile di un package.

14.2 Quando è adatto un package generico

I package generici stanno bene quando, insieme al tipo, si vogliono più operazioni e uno stato interno.

  • Stack a lunghezza fissa
  • Ring buffer
  • Dizionario di piccole dimensioni
  • Suite di statistica
  • Astrazione I/O per dispositivo
  • Suite di operazioni su tipi numerici con unità di misura

In Ada, in particolare, la specifica del package è l’API pubblica e il corpo è l’implementazione: un package generico si usa quindi come «template di modulo type-safe».

Specifica, corpo e formal part di un package genericoIl formal part vincola specifica e corpo; l'utente vede l'API pubblica, l'implementazione resta nascostanascondepackage specAPI pubblicaLato utentepackage bodyimplementazione internageneric formal partcontratto di tipo, valore, funzione

14.3 Partire da pochi parametri formali

Se si aumentano troppo i parametri formali, l’istanziazione diventa illeggibile. È più sicuro partire dal minimo e aggiungerne quando c’è un motivo per sostituirli.

-- Esempio che diventa presto illeggibile
package X is new Generic_Foo
  (A, B, C, D, E, F, G);

-- L'associazione nominata lascia visibile l'intento
package X is new Generic_Foo
  (Element_Type => Integer,
   Index_Type   => Positive,
   Buffer_Size  => 128,
   "<"          => Less);

In Ada, all’istanziazione si può usare l’associazione nominata. I punti importanti del progetto generic appaiono proprio nell’istanziazione, quindi nel codice di produzione scrivere i nomi rende di solito la manutenzione più facile.

15. Punti in cui si inciampa spesso

I generic Ada sono potenti, ma all’inizio ci sono punti in cui è facile inciampare.

15.1 Su un tipo private non si possono fare confronti di ordine

Un corpo come il seguente non si può scrivere.

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      -- errore qui
      return A;
   else
      return B;
   end if;
end Bad_Min;

Element è dichiarato solo private, quindi non è detto che < sia disponibile. Se si vuole confrontare, si aggiunge il confronto al contratto.

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
Confrontare richiede di scrivere il confronto nel contrattoSe il confronto sta nel formal part, all'istanziazione si verifica la confrontabilità; con solo private il corpo non compilaNel corpo si vuole usare un confrontoSi scrive la funzione di confronto nel formal partAll'istanziazione si verifica la confrontabilitàSolo privateErrore di compilazione nel corpo del generic

15.2 is <> non è «inferenza automatica di qualsiasi cosa»

is <> è comodo, ma non è magia. Nel punto di istanziazione deve essere visibile un operatore o un sottoprogramma compatibile. Se la funzione di confronto propria sta in un altro package, è più sicuro fare with o use in modo appropriato, oppure passarla esplicitamente per nome.

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

15.3 Anche le eccezioni sono oggetti distinti per ciascuna istanza

Se si dichiarano eccezioni nella specifica di un package generico, ciascuna istanza ha eccezioni distinte.

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

In questo caso Int_Stack.Stack_Overflow e Float_Stack.Stack_Overflow sono eccezioni diverse. Se si vogliono trattare come eccezione comune, si può valutare di definirle fuori dal generic.

Eccezioni distinte per ciascuna istanzaStack_Overflow di Int_Stack e di Float_Stack, dichiarati in Generic_Stack, sono eccezioni diverseeccezione diversaGeneric_Stackdichiarazione di Stack_OverflowInt_Stack.Stack_OverflowFloat_Stack.Stack_Overflow

15.4 La dimensione del codice può crescere

I generic evitano facilmente indirezioni extra a runtime; d’altra parte, poiché si crea un’istanza per tipo, molti istanziamenti possono far crescere la dimensione del codice.

È lo stesso trade-off dei template C++ e della monomorfizzazione in Rust. Nello sviluppo orientato ad alta affidabilità, embedded o real-time, l’idea è ridurre l’incertezza a runtime e, in cambio, gestire la dimensione di ciò che si genera in build.

Un corpo generic, più codice generatoCiascuna istanza produce codice macchina; si evitano test di tipo e boxing a runtime, ma troppe istanze fanno crescere la dimensioneUn solo corpo genericVersione IntegerVersione FloatVersione Long_FloatVersione My_TypeCodice generatoSi evitano test di tipo e boxing a runtimeCon molte istanze, attenzione alla crescita della dimensione

15.5 Quando conviene limited private

type Element is private; presuppone l’assegnamento. Per oggetti che non si vogliono copiare — handle di file, lock, handle di dispositivo — si valuta limited private.

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

Nei progetti che trattano tipi non copiabili, è più sicuro un algoritmo che applica una procedura, o un progetto che rende esplicito il riferimento, piuttosto che un contenitore che memorizza valori.

16. Una piccola raccolta di pattern di progettazione

Da qui in poi organizziamo in breve le forme che si usano spesso in pratica.

16.1 Offrire Min solo ai valori confrontabili

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;
Generic_Min richiede una funzione di confrontoElement più la funzione di confronto producono Generic_Min, che restituisce il minoreElementServe una funzione di confrontoGeneric_MinRestituisce il minore

16.2 Mettere la soglia in un parametro valore

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;

I parametri valore stanno bene a valori che si vogliono fissare come proprietà dell’istanza, non come impostazione a runtime.

16.3 Iniettare il mezzo di output

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 questa forma si può sostituire la destinazione: output standard, log, buffer di test.

Iniettare Put in Generic_Print_TwicePut come sottoprogramma formale consente output su console, log o buffer di testGeneric_Print_TwiceRiceve Put come sottoprogramma formaleOutput su consoleOutput di logBuffer di test

16.4 Non fissare il tipo di indice dell’array

In Ada anche il tipo di indice di un array è informazione di tipo importante. Invece di inchiodare Positive, rendere all’occorrenza anche il tipo di indice un parametro formale alza il riuso.

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

Con questo progetto si coprono non solo array con indice Positive, ma anche array con indice enumerato.

Index discreto per più tipi di indiceIndex può essere un range Positive, un'enumerazione Day o State, o un tipo intero proprioIndex is discretePositive rangeEnumerazione DayEnumerazione StateTipo intero proprio

17. Checklist per un’API in stile Ada

Quando scrivete un generic, alla fine rileggetelo con questi criteri: diventa più leggibile.

Checklist di progettazione di un genericParametri minimi, operazioni nel formal part, categoria adeguata, associazione nominata, stato ed eccezioni per istanza, dimensione del codice, istanze di testChecklist di progettazione genericI parametri formali sono il minimo?Le operazioni necessarie sono esplicite nel formal part?La categoria private / range / digits è adeguata?Si può istanziare in modo leggibile con l'associazione nominata?Si è consapevoli di stato ed eccezioni per ciascuna istanza?Si può accettare la crescita della dimensione del codice?Si è preparata un'istanza per i test?

In forma testuale, è questo.

  • Le operazioni usate nel corpo devono essere visibili come contratto dei parametri formali.
  • Se basta private, usate private. Se serve aritmetica, usate range <> o digits <>.
  • Confronti, hash, output, conversioni: i comportamenti che cambiano per tipo vanno in un sottoprogramma formale.
  • Se dimensione o soglia sono proprietà dell’istanza, usate un parametro valore.
  • Con stato, pensate prima a un package generico; senza stato, a un sottoprogramma generico.
  • All’istanziazione, più argomenti ci sono, più si usa l’associazione nominata.
  • Progettate presupponendo che eccezioni e stato interno siano indipendenti per ciascuna istanza.

18. Esempio di struttura complessiva dei campioni

Se si vogliono spezzare i campioni dell’articolo in file, una struttura come la seguente è più leggibile.

Struttura di file dei campioniSpecifiche e corpi dei generic in src/generics, demo in src/demosada-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

Per un campione piccolo da articolo, riunire tutto in un file e usare gnatchop è comodo; se però si pensa alla pratica e alla manutenzione di lungo periodo, separare specifica .ads e corpo .adb è la struttura più Ada.

19. Conclusione ── Decidere il confine del riuso con i tipi

La programmazione generic in Ada non è solo «una funzione per scrivere codice indipendente dal tipo». L’essenza è rendere esplicito, come contratto di tipi, sottoprogrammi e valori, che cosa un componente riusabile richiede.

Dal contratto al riuso type-safeSi scrive il contratto, si scrive il corpo, si istanzia passando tipo, valore e funzione, e si riusa senza copiareSi scrive il contrattoSi scrive il corpo del genericSi istanzia passando tipo, valore, funzioneSi usa in modo type-safeSi riusa senza copiare

Come abbiamo visto, nei generic Ada si possono rendere parametri formali i seguenti.

  • Tipi
  • Valori
  • Sottoprogrammi
  • Package

Inoltre, sui tipi si possono specificare categorie piuttosto fini: private, limited private, range <>, mod <>, digits <>, delta <>, (<>). Così il corpo del generic non dipende da «operazioni di cui non si sa se sono possibili»: si implementa in modo sicuro solo con le operazioni scritte nel contratto.

Nel patrimonio C o C++ più vecchio, per il riuso si usano a volte macro, void*, puntatori a funzione, ramificazioni di tipo scritte a mano. I generic Ada possono sostituire gran parte di questi usi in una forma type-safe e leggibile. In particolare, nella manutenzione di lungo periodo, nell’embedded, nel real-time e nel software ad alta affidabilità, questa «progettazione che fissa i confini in compilazione» ha un valore grande.

20. Aree di consulenza correlate

KomuraSoft LLC si occupa di sviluppo di applicazioni Windows, indagine e modifica di asset esistenti, riordino dei confini COM / ActiveX / 32 bit / 64 bit, consulenza tecnica e revisione della progettazione. Oltre alla progettazione a tipizzazione statica e alta affidabilità come quella di Ada, un tema vicino nella pratica è anche come riordinare, prolungare e migrare asset C/C++, C#, VB6, MFC e COM esistenti.

Riferimenti

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Che cosa sono i generic in Ada?
Sono un meccanismo di riuso che accetta tipi, valori, sottoprogrammi e package interi come parametri formali, e li controlla staticamente al momento dell'istanziazione con new. Non si tratta di una semplice sostituzione di testo: in compilazione si stabilisce se quel componente soddisfa il contratto. Un sottoprogramma generico non è chiamabile solo perché è dichiarato; diventa una procedura o una funzione ordinaria solo dopo l'istanziazione con un tipo concreto.
In che cosa i generic Ada differiscono dai template C++?
Ada adotta fin dall'inizio un contract model in cui il contratto è esplicito: il corpo del generic è scritto usando solo le operazioni promesse dai parametri formali e viene type-checkato da solo. I template C++ hanno avuto storicamente la proprietà che gli errori emergono solo all'istanziazione; C++20 con i concepts ha migliorato la situazione. Inoltre Ada, oltre ai parametri valore e sottoprogramma, può prendere un package intero come parametro formale, e così si possono combinare componenti generic tra loro.
Perché nei parametri di tipo formali Ada si specifica una categoria di tipo?
Per rendere esplicito, come contratto, quali operazioni il corpo può usare. Con type T is private si possono presupporre solo operazioni di base come assegnamento e uguaglianza; confronti di ordine e aritmetica non sono disponibili. Se serve aritmetica intera si usa range <>, se serve aritmetica in virgola mobile digits <>, se servono operazioni bit a bit mod <>. La categoria funziona anche come specifica a livello di tipo che comunica al lettore che cosa il componente richiede.
Ci sono punti di attenzione con i generic Ada?
Poiché si crea un'istanza per tipo, molti istanziamenti possono far crescere la dimensione del codice. È lo stesso trade-off dei template C++ e della monomorfizzazione in Rust. Inoltre le eccezioni dichiarate nella specifica di un package generico sono eccezioni distinte per ciascuna istanza. In pratica si parte dal minimo di parametri formali e se ne aggiungono solo quando c'è un motivo per sostituirli, e si usa l'associazione nominata quando l'istanziazione ha molti argomenti.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog