Ada (linguaggio di programmazione)

Ada è un linguaggio di programmazione sviluppato verso la fine degli anni settanta su iniziativa del Dipartimento della Difesa (DOD) degli Stati Uniti. Sia le specifiche che lo sviluppo del linguaggio furono affidati a bandi di gara. Tra le 17 proposte inviate in seguito al bando indetto dal DOD, fu scelto nel 1979 il progetto di Jean Ichbiah, che all'epoca lavorava presso la CII Honeywell Bull. Le specifiche divennero uno standard ANSI e ISO nel 1983, seguito dalle successive revisioni nel 1995, 2005 e 2012. Un sottoinsieme hard real-time del linguaggio è noto come profilo Ravenscar.

Ada combina principi e tecniche provenienti da diversi paradigmi di programmazione, in particolare programmazione modulare, programmazione orientata agli oggetti, programmazione concorrente e calcolo distribuito. Sebbene l'interesse del DOD vertesse principalmente sullo sviluppo di applicazioni militari, Ada è un linguaggio general purpose che si presta all'utilizzo in qualsiasi dominio applicativo. L'origine militare si rivela però nella presenza di caratteristiche fortemente orientate alla sicurezza del codice; per questo motivo, il linguaggio viene ancora oggi usato in molti contesti in cui il corretto funzionamento del software è critico, come astronautica, avionica, controllo del traffico aereo, finanza e dispositivi medici.

I compilatori Ada impiegati per lo sviluppo di software mission-critical devono seguire un processo di certificazione secondo lo standard internazionale ISO/IEC 18009 (Ada: Conformity Assessment of a Language Processor), implementato nella suite Ada Conformity Assessment Test Suite (ACATS), parte integrante del processo di certificazione svolto da laboratori autorizzati dall'Ada Compiler Assessment Authority (ACAA).

Il nome iniziale del linguaggio doveva essere DOD-1, ma venne in seguito cambiato in Ada in onore di Ada Lovelace, illustre matematica dei primi anni del XIX secolo, accreditata come la prima programmatrice della storia per aver sviluppato un algoritmo per il calcolo dei numeri di Bernoulli sulla macchina analitica di Charles Babbage.

Caratteristiche 
Ada eredita alcune caratteristiche stilistiche fondamentali da ALGOL, rispetto al quale aggiunge molte funzionalità basilari (come il sistema di tipi, i record, i puntatori o le enumerazioni, implementati in buona parte in stile Pascal) e funzionalità avanzate proprie dei moderni linguaggi di programmazione (polimorfismo, ereditarietà, eccezioni, tasking).

Il linguaggio fornisce un gran numero di controlli sia statici (a tempo di compilazione) sia dinamici (a runtime), che prevengono un'ampia varietà di errori (uso errato dei parametri, errori di tipo, violazione di range e off-by-one). I controlli dinamici sono disattivabili se si vuole massimizzare l'efficienza, tramite il pragma Suppress, o tramite switch specifici dei vari compilatori. Sicurezza e affidabilità del codice sono infatti uno dei principali aspetti che hanno guidato lo sviluppo del linguaggio.

"Hello, world!" in Ada 
Un Hello world in Ada è il seguente:

with Ada.Text_IO; use Ada.Text_IO;
procedure Hello is
begin
  Put_Line("Hello, world!");
end Hello;

Sintassi 
A differenza della maggior parte dei linguaggi di programmazione, Ada è insensibile alle maiuscole. Le convenzioni stilistiche più comuni prevedono di scrivere le keyword del linguaggio interamente in minuscolo (o, meno comunemente, interamente in maiuscolo) e i nomi di tutti gli identificatori (tipi, variabili, subroutine, package etc.) in snake case con l'iniziale di ogni parola maiuscola. I commenti sono su singola linea, iniziati da un doppio trattino -- e terminati dal ritorno a capo. Per gli identificatori e i commenti Ada supporta il set di caratteri ISO 10646, permettendo quindi di scrivere codice in caratteri non ASCII, e la lunghezza massima consentita per gli identificatori è legata all'implementazione (nelle implementazioni standard deve essere pari a non meno di 200 caratteri).

La sintassi di Ada impiega pochi simboli, prediligendo l'uso di parole in lingua inglese. I simboli sono l'assegnazione :=, gli operatori aritmetici elementari (+, -, *, / e **), gli operatori di comparazione (<, <=, >, >=, =, /=), i due punti : per dichiarare il tipo di una variabile, la freccia => negli aggregati, la virgola , come separatore negli aggregati, l'operatore di accesso ai campi ., l'indicazione di range .., il tick ' per accedere agli attributi, il box <> per indicare un parametro variabile nelle dichiarazioni, il punto e virgola ; per separare i parametri di una routine e per terminare le istruzioni. L'istruzione nulla è costituita dalla keyword null terminata da punto e virgola, mentre una riga vuota terminata da punto e virgola non è valida. I restanti operatori sono implementati tramite keyword (and, or, rem, mod etc.).

Ada è un linguaggio strutturato e i blocchi sono delimitati da keyword. La chiusura del blocco contiene un riferimento o identificatore per capire immediatamente a quale apertura appartiene ed evitare confusione, impedendo inoltre potenziali dangling else o analoghi errori. Ad esempio, una subroutine contenente un ciclo for appare come:

procedure Example is
  X: array (Integer range 1..5) of Integer;
begin
  for I in X'Range loop
    X(I) := I;
  end loop;
end Example;

Parole riservate 
Il linguaggio, in riferimento allo standard Ada 2012, ha 73 parole riservate:

abort
abs
abstract
accept
access
aliased
all
and
array
at
begin
body
case
constant
declare
delay
delta
digits
do
else
elsif
end
entry
exception
exit
for
function
generic
goto
if
in
interface
is
limited
loop
mod
new
not
null
of
or
others
out
overriding
package
pragma
private
procedure
protected
raise
range
record
rem
renames
requeue
return
reverse
select
separate
some
subtype
synchronized
tagged
task
terminate
then
type
until
use
when
while
with
xor

Esiste un'omonimia tra le parole riservate access, delta, digits, mod e range e gli omonimi attributi, ma essendo gli attributi sempre preceduti da un apostrofo (detto tick) non esiste rischio di ambiguità.

Dichiarazioni di variabile 
Le dichiarazioni di variabile in Ada si effettuano indicando il nome dell'entità dichiarata, seguito da : e poi dal tipo, quest'ultimo preceduto da eventuali specificatori (constant, aliased, not null etc.). Un valore di inizializzazione statico o dinamico può essere indicato facoltativamente dopo il tipo, separato da :=, e la dichiarazione è infine terminata da ;. Le dichiarazioni possono essere collocate nella specifica di un package o nella parte dichiarativa di una routine o di un blocco declare. Più variabili possono essere dichiarate nella stessa dichiarazione, separando i nomi con virgole, ma si tratta solo di zucchero sintattico e tecnicamente le dichiarazioni sono comunque distinte ed indipendenti fra loro, quindi ad esempio un'eventuale espressione di inizializzazione viene valutata una volta per ciascuna variabile.

È possibile definire variabili costanti facendo precedere il tipo dalla keyword constant, nel quale caso è obbligatorio fornire un'inizializzazione, ed è inoltre possibile definire costanti numeriche omettendo il tipo prima dell'inizializzazione, che deve consistere in un'espressione statica, ovvero nota a tempo di compilazione (il tipo della costante viene inferito dall'inizializzatore letterale). La differenza tra variabili costanti e costanti numeriche consiste nel fatto che le prime sono variabili calcolate a runtime, mentre le seconde vengono risolte a tempo di compilazione.

-- variabile intera
N: Integer;

-- due variabili intere, inizializzate con un valore casuale
-- (possono assumere due valori diversi)
I, J: Integer := Random(Seed);

-- variabile costante (valutata a runtime)
F: constant Float := Float(I + J);

-- costante numerica (risolta staticamente a tempo di compilazione)
π: constant := 3.14159_26535_89793_23846;

Goto 
A differenza di molti linguaggi moderni, Ada conserva l'istruzione goto. Pur essendo generalmente deprecata nella programmazione strutturata, è infatti un'istruzione utile per la generazione automatica di codice a partire da sorgenti in altri linguaggi o specifiche formalizzate ad alto livello. È inoltre accettabile in alcuni contesti, come il salto verso il termine di un blocco (ad esempio un'iterazione di un ciclo) o l'uscita da cicli profondamente annidati, contesto nel quale è più leggibile rispetto ad analoghe istruzioni break o continue in altri linguaggi in quanto il punto di arrivo del salto nel codice è chiaramente indicato dalla label (mentre il break in linguaggi come il C può essere error prone).

procedure Print_Integers is
begin
  for I in 1..20 loop
    Put(Integer'Image(I));
    if I mod 5 = 0 then
      New_Line;
      goto Continue; -- goto
    end if;
    Put(", ");
  <<Continue>> -- label
  end loop;
end Print_Integers;

Sistema di tipi 

Ada è un linguaggio fortemente tipizzato e typesafe. A differenza del C e dei suoi derivati, Ada non ammette conversioni di tipo implicite (tranne che per i tipi universali e gli access type anonimi) e la conversione di tipo si effettua scrivendo il nome del tipo seguito dalla variabile da convertire tra parentesi, ad esempio:

procedure Conversion is
  X: Float := 2.9;
  Y: Integer;
begin
  Y := X;          -- illegale (assegna valore Float a variabile Integer)
  X := X + 1;      -- illegale (l'operatore + non è definito su tipi misti)
  X := X + 1.0;    -- legale
  Y := Integer(X); -- conversione di tipo (con arrotondamento): ora Y vale 3
end Conversion;

Il tipo di un letterale può essere indicato esplicitamente con una sintassi simile alla conversione, aggiungendo un tick prima della parentesi aperta. Questo è utile soprattutto in caso di ambiguità, quando uno stesso letterale (ad esempio una stringa, o un valore enumerativo) può riferirsi a tipi diversi.

Tipi predefiniti 
A differenza del C e dei suoi derivati, in Ada non esistono keyword che identificano tipi primitivi, e tutti i tipi predefiniti sono definiti nella standard library. I tipi predefiniti fondamentali (come Integer, Float o Character) sono definiti nel package Standard, ma per massimizzare la portabilità è considerata buona pratica non usare direttamente i tipi predefiniti e definire invece i propri, anche per i tipi numerici più semplici.

I tipi definiti nel package Standard sono sempre visibili in ogni parte del programma, mentre gli altri tipi definiti nei restanti package della libreria standard richiedono una clausola with per essere visibili. I tipi definiti in Standard sono:
 Integer: tipo discreto intero, che assume valori in un intervallo di estensione legata all'implementazione, non inferiore a  .. .
 Natural: sottotipo di Integer che può assumere solo valori non negativi;
 Positive: sottotipo che può assumere solo valori positivi non nulli;
 Float: tipo in virgola mobile con almeno sei cifre;
 Duration: tipo in virgola fissa, usato per esprimere un tempo in secondi;
 Character, Wide_Character, Wide_Wide_Character: tipi speciali di enumerazione, usati per i caratteri di testo con codifica rispettivamente a 8, 16 e 32 bit (gli ultimi due sono stati aggiunti rispettivamente in Ada 95 e in Ada 2005)
 String, Wide_String, Wide_Wide_String: stringhe di lunghezza fissa, costituite da array di caratteri. Altre due varietà di stringhe, più flessibili ma anche più pesanti dal punto di vista computazionale, sono definite in altri package della libreria, e sono Ada.Strings.Bounded.Bounded_String (per stringhe di lunghezza variabile fino ad un valore massimo) e Ada.Strings.Unbounded.Unbounded_String (per stringhe di lunghezza variabile senza restrizioni), i cui rispettivi package forniscono anche routine per la manipolazione;
 Boolean: tipo enumerativo che può assumere i valori False e True, e che gode di una semantica particolare.

I package System e System.Storage_Elements forniscono alcuni tipi che sono utili per la programmazione a basso livello:
 System.Address: rappresenta un indirizzo di memoria;
 System.Storage_Elements.Storage_Offset: rappresenta un offset di memoria, che può essere aggiunto o sottratto ad un indirizzo per ottenere un altro indirizzo;
 System.Storage_Elements.Storage_Count: un sottotipo di Storage_Offset che può assumere solo valori non negativi, usato per rappresentare la dimensione di una struttura dati;
 System.Storage_Elements.Storage_Element: rappresenta l'unità minima di memoria indirizzabile (ovvero un singolo byte nella maggior parte delle implementazioni);
 System.Storage_Elements.Storage_Array: array di Storage_Element.

Dichiarazione di tipi e sottotipi 
I tipi possono essere definiti usando la keyword type, sotto forma di enumerazione, come tipo in virgola mobile con la keyword digits o a partire da un altro tipo preesistente, usando la keyword new, mentre l'attributo 'Base di un tipo fornisce il tipo base da cui deriva. A partire da un tipo è possibile definire un sottotipo (con la keyword subtype), che consiste in un insieme di valori contenuto nel dominio del tipo da cui deriva.

-- nuovo tipo numerico definito a partire da Integer
type Giorno is new Integer;

-- sottotipo di Giorno
subtype Giorno_Del_Mese is Giorno range 1..31;

Tipi enumerativi 
I tipi enumerativi possono essere specificati indicando tra parentesi i possibili valori. I letterali di tipo enumerativo possono confliggere, e in tale caso per risolvere l'ambiguità si indica il tipo esplicitamente. È possibile specificare un range di valori enumerativi, ed è possibile dichiarare sottotipi enumerativi che assumano solo una parte dei valori del tipo da cui derivano.

Per i tipi enumerativi sono definiti automaticamente gli operatori di confronto (l'ordine è dato dalla posizione nella dichiarazione, partendo dal valore minimo verso il massimo), e dispongono di diversi attributi utili, tra i quali 'First e 'Last, che restituiscono il primo e l'ultimo valore del tipo, 'Succ e  'Pred, che restituiscono i valori precedente e successivo, 'Pos e 'Val, che restituiscono rispettivamente l'indice posizionale (zero-based) del valore (ovvero la posizione nella dichiarazione) e il valore associato ad un indice.

procedure P is
  type Divinità is (Giove, Giunone, Minerva, Eros, Venere);
  type Pianeta is (Mercurio, Venere, Terra, Marte, Giove, Saturno, Urano, Nettuno);
  subtype Pianeta_Roccioso is Pianeta range Mercurio .. Marte;

  P1: Pianeta  := Mercurio;
  D1: Divinità := Giunone;

  -- letterali ambigui
  P2: Pianeta  := Pianeta'(Giove);
  D2: Divinità := Divinità'(Giove);

  B: Boolean;
  I: Integer;
  D: Divinità;
  P: Pianeta;
begin
  -- esempi di operatori e attributi
  B := Giunone < Minerva;       -- True
  I := Pianeta'Pos(Venere);     -- 1
  P := Pianeta_Roccioso'Last;   -- Marte
  D := Divinità'Pred(Minerva);  -- Giunone
end P;

Array 
Gli array sono definiti tramite la keyword array, specificando tra parentesi la natura degli indici (che possono essere di un qualsiasi tipo discreto). Oltre al tipo è possibile specificare un range per gli indici dell'array, indicando non la dimensione della struttura, ma una coppia di valori minimo e massimo per l'indice, eliminando alla radice il problema della scelta tra array zero-based e one-based. L'accesso agli elementi di un array avviene segnando l'indice tra parentesi tonde.

Un tipo array può essere definito anonimamente nella dichiarazione di variabile, oppure può essere dichiarato un tipo specifico per l'array con la keyword type. La differenza fondamentale è che nel primo caso due array non possono essere assegnati fra loro, anche se dichiarati con un tipo anonimo che sembra lo stesso, in quanto si tratta formalmente di due tipi diversi (anche se la dichiarazione avviene nella stessa riga separando i nomi delle variabili con la virgola, in quanto tale notazione è solo zucchero sintattico per due dichiarazioni distinte).

Gli array supportano lo slicing, assegnando contemporaneamente un range (non necessariamente statico) di indici, anche con sovrapposizione di elementi.

procedure P is
  -- array di tipo anonimo
  A, B: array (Integer range 1..5) of Float;
  
  -- tipo per array, di lunghezza variabile
  type Vettore is array (Integer range <>) of Float;
  -- sottotipo per array di lunghezza fissa
  subtype Vettore_5 is Vettore(1..5);

  -- array con indice 1..5
  C, D: Vettore_5;

  -- array con indice 1..6
  E: Vettore (1..6);
begin
  A := B; -- illegale (non compila), le due variabili hanno tipo (anonimo) diverso
  C := D; -- legale
  C := E; -- illegale

  A(1..3) := B(2..4); -- slice
  A(1..3) := A(2..4); -- slice con sovrapposizione di elementi (ok)
end P;

È possibile dichiarare array mutlidimensionali nativi, separando gli indici con virgole, oppure sotto forma di array di array (e quindi jagged array), in questo caso avendo cura di fare attenzione all'ordine delle dichiarazioni.

procedure P is
  A: array (Integer range 1..5, Integer range 2..7) of Float;           -- array di dimensione 2
  B: array (Integer range 2..7) of array (Integer range 1..5) of Float; -- array di array
begin
  A(1, 2) := 5.0; -- accesso ad un elemento di un array multidimensionale
  B(1)(2) := 5.0; -- accesso ad un elemento di un array di array
end P;

Per gli array è possibile utilizzare nelle dichiarazioni o nelle istruzioni dei letterali (detti aggregati), costituiti da un insieme di valori tra parentesi tonde, separati da virgole. Gli aggregati possono essere posizionali oppure nominativi: nel primo caso la corrispondenza del valore con l'indice cui si riferisce è data dalla posizione nell'aggregato (l'ordine dei valori deve seguire quindi quello degli elementi dell'array), nel secondo caso invece per ogni valore si specifica l'indice, usando il simbolo => per separarlo dal valore. È possibile assegnare uno stesso valore a più indici, usando il range .. se sono consecutivi o una pipe | per elencare dei valori non consecutivi. La sintassi degli aggregati è rigorosa e non consente inizializzazioni parziali, ma è possibile specificare individualmente solo alcuni elementi e completare l'assegnazione con un valore per tutti i restanti elementi, usando la keyword others.

-- aggregato posizionale
A: array (Integer range 1..5) of Integer := (1, 2, 3, 4, 5);

-- aggregato nominativo, con range e others (gli aggregati per gli array annidati sono invece posizionali)
B: array (Integer range 2..7) of array (Integer range 1..5) of Integer
  := (2 .. 4 | 6 => (1, 3, 5, 7, 9), others => (2, 4, 6, 8, 10));

È possibile applicare agli array unidimensionali di valori Boolean che abbiano stesso tipo e lunghezza gli operatori not, and, or e xor, e il risultato è un array le cui componenti sono calcolate applicando l'operatore alle singole coppie di componenti degli array operandi.

Record 
I record possono essere definiti con la keyword record, specificando i campi analogamente alle dichiarazioni di variabili (inclusa la possibilità di assegnare valori predefiniti). È possibile accedere ai campi di un record con la dot notation, ed utilizzare aggregati costruiti analogamente a quelli degli array. È inoltre possibile definire un record senza campi con la coppia di keyword null record, utile per definire tipi astratti o per estendere un tipo senza aggiungere nuovi campi.

procedure P is
  -- record
  type Data is
    record
      Giorno : Integer range 1..31 := 1;
      Mese   : Nome_Mese           := Gennaio; -- Nome_Mese è un qualche tipo enumerativo
      Anno   : Integer             := 1970;
    end record;

  D, E: Data;

  type Null_Record is null record; -- record senza campi
begin
  D := (10, Gennaio, 1995);
  E.Giorno := D.Giorno;
end P;

Tipi parametrici 
È possibile utilizzare dei parametri (detti discriminanti) nella dichiarazione di un tipo, che verranno specificati quando si dichiarano le variabili o si deriva un tipo non parametrico.

package Discriminanti is
  -- tipo per un testo di lunghezza generica, specificata dal discriminante Size
  type Testo(Size:  Positive) is
    record
      Position :  Positive :=  1;
      Data     :  String(1 .. Size);
    end record;

  -- variabile per un testo di 100 caratteri
  T: Testo (100);
end Discriminanti;

Tipi limitati 
Un ulteriore meccanismo di controllo sui tipi è dato dalla possibilità di definire tipi limitati, tramite la keyword limited. I tipi limitati dispongono solo delle operazioni dichiarate nel package, quindi non sono assegnabili con := e non dispongono delle operazioni di confronto predefinite = e /=. Un tipo privato può essere definito limitato ma implementato internamente come non limitato, in questo caso il tipo è limitato all'esterno del package ma non limitato nella parte privata dello stesso, garantendo flessibilità all'interno del package, dove le operazioni predefinite rimangono disponibili, e allo stesso tempo il controllo sul tipo rispetto a chi lo usa all'esterno del package.

Modularità 
Ada è un linguaggio strutturato e permette di dividere il codice in più unità di compilazione (routine e package), che possono essere compilate separatamente.

Routine 
Ada supporta due tipi di routine, funzioni e procedure, dichiarate con le keyword function e  procedure. Le prime hanno un valore di ritorno e possono essere usate solo nel contesto di un'espressione (in Ada, a differenza dei linguaggi derivati dal C, non esistono expression statement), mentre le seconde non hanno valore di ritorno e possono essere usate solo come istruzione. Sia le procedure sia le funzioni sono costituite da due parti distinte, una parte dichiarativa tra le keyword is e begin, che può contenere solo dichiarazioni (come variabili, tipi o altre routine annidate), e una parte contenente solo istruzioni, tra le keyword begin e end. È possibile inserire dichiarazioni fuori dalla parte dichiarativa usando un blocco declare, e le relative dichiarazioni sono visibili solo al suo interno. Una routine può costituire un'unità di compilazione autonoma oppure può essere dichiarata ed implementata all'interno di un package o nella parte dichiarativa di un'altra routine.

I parametri di una routine vengono specificati tra parentesi tonde, e Ada supporta il passaggio di parametri sia per valore sia per riferimento, con tre diversi modi: in, out e in out. I parametri passati in modo in possono solo essere letti dalla routine, quelli in modo out possono solo essere scritti, quelli in modo in out possono essere letti e scritti. Il modo di un parametro viene specificato dopo i : e prima del tipo, e se omesso viene assunto in come predefinito. È possibile definire un valore predefinito per i parametri, che in questo modo diventano opzionali e possono essere omessi nelle chiamate alla routine: in tale caso viene usato il valore predefinito. Per i parametri e i valori di ritorno di tipo access è possibile specificare la null exclusion, che causa un'eccezione a runtime se il parametro (o il valore restituito) sono null. Le routine vengono invocate facendo seguire al nome delle stesse una coppia di parentesi tonde contenenti i parametri, ma se una routine viene dichiarata o chiamata senza passare alcun parametro le parentesi tonde possono essere omesse. Le routine definite su tipi tagged possono essere richiamate anche con la dot notation rispetto al parametro di tipo tagged.

-- esempio di procedura con parametro facoltativo
procedure Incrementa(X: in out Integer; Incremento: Integer := 1) is
begin
  X := X + Incremento;
end Incrementa;

-- esempio di chiamata in un'altra porzione di codice
procedure P is
  N: Integer := 0;
begin
  Incrementa(N);
  -- ora N vale 1
  Incrementa(N, 3);
  -- ora N vale 4
end P;

Anche gli operatori sono funzioni, definite indicando il nome dell'operatore tra doppi apici, e Ada supporta l'overloading degli stessi, così come l'overload e l'override delle routine in generale.

-- esempio di overload dell'operatore + con operandi di un tipo T
-- la cui somma è definita nella funzione Somma_T
function "+"(Left, Right: T) return T is
begin
  return Somma_T(Left, Right);
end "+";

Gli operatori sono identificati dalle seguenti keyword:

abs and mod not or rem xor
= /= < <= > >=
+ - * / ** &

Package 
La modularità del codice ad un livello più astratto rispetto alle routine è ottenuta tramite i package. Un package è suddiviso in due parti distinte, specifica e implementazione, e alcuni compilatori (come GNAT) obbligano ad inserire una sola unità di compilazione per file, dividendo inoltre specifica e implementazione in due file separati (rispettivamente .ads e .adb). La specifica rappresenta l'interfaccia del package accessibile dall'esterno e può essere compilata indipendentemente dall'implementazione, facilitando i test nelle fasi iniziali di sviluppo. L'implementazione contiene il codice effettivo delle routine definite nell'interfaccia, più altre eventuali routine non accessibili dall'esterno del package. I package possono essere compilati separatamente e, in caso di modifica all'implementazione di un package senza alterarne la specifica, eventuali altri package da esso dipendenti non necessitano di essere ricompilati.

Le dichiarazioni delle routine contenute devono essere inserite nella specifica del package (file .ads), mentre l'implementazione si colloca nel body (file .adb).  Specifica e implementazione devono avere una type conformance completa, ovvero il nome della routine, l'eventuale tipo di ritorno, il numero, tipo, modo, ordine, valore predefinito e nome dei parametri devono essere identici nella specifica e nell'implementazione, a meno di alcune differenze non sostanziali: un letterale numerico può essere sostituito con uno formalmente diverso ma con lo stesso valore, a un identificatore può essere aggiunto un prefisso in dot notation, un'indicazione esplicita del modo in può essere omessa, un insieme di parametri dello stesso sottotipo possono essere indicati separatamente. Tutte le routine dichiarate nella specifica devono essere necessariamente implementate nel body, salvo alcune eccezioni: le procedure nulle (la cui dichiarazione termina con is null, ed è equivalente a una procedura contenente solo un'istruzione nulla), le routine astratte (che non hanno un'implementazione) e le funzioni costituite da un'espressione condizionale (che viene inserita direttamente nella specifica).

-- specifica
package P is
  -- routine implementate nel body
  procedure Somma(A, B: in Integer; S: out Integer);
  function Somma(A, B: Integer := 0) return Integer;

  -- routine non implementate nel body
  procedure Foo(A: Integer) is null;         -- procedura nulla
  function Baz is abstract;                  -- routine astratta
  function Bar(A: Integer) return Boolean is -- funzione costituita da un'espressione condizionale
    (if A > 0 then True else False);
end P;

-- implementazione
package body P is
  procedure Somma(A, B: in Integer; S: out Integer) is
  begin
    -- inizio istruzioni
    S := A + B;
  end Somma;

  function Somma(A, B: Integer := 0) return Integer is
    -- inizio parte dichiarativa
    C: Integer;
  begin
    -- inizio istruzioni
    C := A + B;
    return C;
  end Somma;
end P;

Si può creare una gerarchia di package in due modi: con l'annidamento, definendo un package dentro l'altro, e con la parentela, definendo un package come figlio dell'altro (separando padre e figlio con la dot notation). I package figli possono essere inoltre definiti come privati, e in questo modo sono totalmente inaccessibili fuori dal package padre.

-- package padre
package Padre is
    -- package annidato
    package Annidato is
      ...
    end Annidato;
end Padre;

-- package figlio
package Padre.Figlio is
   ...
end Padre.Figlio;

-- package figlio privato
private package Padre.Figlio_Privato is
  ...
end Padre.Figlio_Privato;

 Tali tipi avranno perciò una vista parziale nella parte pubblica, e una vista completa nella parte privata del package. I package figli condividono la parte privata del padre ma non dei fratelli, tuttavia è possibile importare un package fratello in modo da poterne vedere anche la parte privata usando la clausola private with invece della solita with.

package P is
  type T is private; -- T è visibile fuori da P, ma non la sua implementazione

private
  -- S non può essere visto né usato fuori dal package P
  type S is
    record
      ...
    end record;

  -- completa la definizione di T
  type T is
    record
      -- i campi di T non sono visibili né utilizzabili fuori dal package P
      I: Integer;
    end record;
end P;

Generics 
In Ada le routine o i package possono avere dei parametri risolti staticamente a tempo di compilazione, specificati facendo precedere l'unità dalla keyword generic e specificati tra parentesi tonde quando si istanzia l'unità generica. I generics sono un meccanismo statico che consente un riuso del codice, permettendo di istanziare la stessa unità con parametri di tipo differenti, ma a differenza di altri linguaggi i parametri generici possono essere non solo tipi, ma anche valori o routine. Nel caso di package generici, non può essere usata su essi la clausola use (può essere usata solo sulle loro istanze concrete) ed ogni eventuale loro package figlio deve necessariamente essere generico.

Ad esempio, una procedura per scambiare due elementi può essere parametrizzata rispetto al tipo degli stessi

generic
  type T is private;
procedure Scambia(A, B: in out T);

procedure Scambia(A, B: in out T) is
  Temp: T;
begin
  Temp := A;
  A := B;
  B := Temp;
end Scambia;

ed essere istanziata per l'uso su tipi diversi:

procedure Scambia_Interi is new Scambia (Integer);
procedure Scambia_Float  is new Scambia (Float);

Analogamente un package può implementare uno stack generico

generic
  type T is private;
  Capacità: Positive;
package Stack is
  procedure Push(X: T);
  function Pop return T;
private
  A: array (1 .. Capacità) of T;
  Posizione: Integer range 0 .. Capacità;
end Stack;

che può essere istanziato per contenere oggetti di differente tipo e scegliendo di volta in volta la capacità massima:

package Int_Stack   is new Stack (Integer, 100); -- stack con una capacità di 100 valori interi
package Float_Stack is new Stack (Float, 50);    -- stack con una capacità di  50 valori float

Parametri di tipo 
Nel caso più semplice, un parametro di tipo generico è istanziabile e ha solo la definizione di assegnamento e uguaglianza. Possono essere specificati diversi attributi del parametro di tipo, che ne modificano l'interfaccia: può essere limitato, può avere dei discriminanti (anche non specificati, indicati con il box <>, in tal caso il tipo non è istanziabile), può essere tagged, astratto, può essere discendente di un tipo preesistente o può essere un'interfaccia con una o più interfacce progenitrici. Alcuni esempi di parametri di tipo generici:
generic
  type A is private;               -- tipo privato
  type B is limited;               -- tipo limitato
  type C (X: Integer);             -- tipo con discriminante X
  type D (<>);                     -- tipo con discriminante sconosciuto
  type E is tagged;                -- tipo tagged
  type F is new T;                 -- tipo derivato dal tipo non tagged T
  type G is new T with private;    -- tipo derivato dal tipo tagged T
  type H is interface and I and J; -- tipo interfaccia con due progenitrici
package Generic_Package is
  ...
end Generic_Package;

Per i tipi enumerativi e numerici esistono anche delle indicazioni particolari, e per ognuno di essi sono disponibili le operazioni predefinite per tale tipo.

generic
  type A is (<>);               -- tipo enumerativo
  type B is range <>;           -- tipo numerico con segno
  type C is mod <>;             -- tipo numerico modulare
  type D is digits <>;          -- tipo in virgola mobile
  type E is delta <>;           -- tipo in virgola fissa
  type F is delta <> digits <>; -- tipo decimale

Parametri routine 
Poiché per un tipo generico si assume l'esistenza delle sole funzioni predefinite per quella categoria di tipi, può essere necessario passare come parametro anche una o più routine. Ad esempio, implementando un albero binario di ricerca che possa contenere chiavi di un tipo qualsiasi, per effettuare le operazioni di inserimento o ricerca è necessario poter comparare le chiavi, ma l'operatore di confronto < non è predefinito per ogni tipo. Si può quindi ovviare al problema passando tale operatore come parametro generico. I parametri generici per le routine devono essere quindi passati esplicitamente quando si istanzia il package, ma possono essere omessi se nella dichiarazione del parametro generico si specifica is <>. In questo caso la routine viene scelta automaticamente quando si istanzia il package, a patto che sia definita e visibile, sia unica e abbia completa type conformance con la dichiarazione del parametro generico.

-- package generico
generic
  type T is private;
  with function "<"(Sinistra, Destra: T) return Boolean is <>;
package Albero_Binario is
  ...
end Albero_Binario;

-- package che usa l'Albero_Binario
package P is
  type Data is
    record
      ...
    end record;
  function Confronta_Data(Sinistra, Destra: Data) return Boolean;

  -- istanzia il package generico sul tipo Data
  package Albero_Data is new Albero_Binario(Data, Confronta_Data);

  -- l'operatore può anche essere omesso, perché esiste ed è visibile per il tipo Float
  package Albero_Float is new Albero_Binario(Float);
end P;

Memoria dinamica 
La memoria dinamica (chiamata storage pool) è gestita ad alto livello ed è typesafe. Il linguaggio non fornisce puntatori flessibili come quelli del C (che sono una tra le principali fonti di bug, errori e vulnerabilità), ma utilizza dei riferimenti (detti access type) che conservano informazioni sull'accessibilità degli oggetti ai quali fanno riferimento e seguono precise regole di accessibilità, prevenendo il problema dei dangling pointer. La versatilità del linguaggio per applicazioni a basso livello è comunque garantita, grazie all'attributo 'Address e al package System, che permettono di manipolare indirizzi di memoria raw. L'interfaccia Interfaces.C.Pointers fornisce inoltre dei puntatori in stile C, utili quando si interfaccia un'applicazione Ada con una in C.

La semantica del linguaggio permette la garbage collection, la cui presenza è però legata all'implementazione: solitamente è assente nei compilatori per le architetture native (in quanto può influire in maniera imprevedibile sul timing, e questo è un effetto deleterio nei sistemi real-time) ma è talvolta presente per i compilatori che hanno come architettura target la JVM. La deallocazione può essere effettuata manualmente, istanziando l'unità generica Ada.Unchecked_Deallocation, che deve essere usata con attenzione per evitare di deallocare oggetti nello stack o creare dangling pointer. Per aumentare la sicurezza si può applicare un pattern di smart pointer, creando oggetti che contano e gestiscono autonomamente i riferimenti alle risorse, in modo che il programmatore del client non debba deallocare niente in maniera esplicita.

Programmazione orientata agli oggetti 
A differenza di molti linguaggi orientati agli oggetti, Ada non ha un costrutto per le classi analogo a C++ o Java. Tra i principali aspetti della programmazione OOP vi sono la possibilità di distinguere il tipo di un oggetto a runtime, di definire un tipo a partire da un altro e di permettere a tali tipi di ereditare le operazioni primitive del tipo da cui deriva. In Ada la differenza tra variabili che sono "oggetti" e che non lo sono consiste nel fatto che le prime conservano a runtime le informazioni sul proprio tipo, consentendo il polimorfismo e il dynamic dispatch.

Ereditarietà 
L'ereditarietà si ha tramite estensione, che rende possibile aggiungere nuovi campi, mantenendo anche quelli ereditati. È possibile convertire un oggetto (record tagged) di un sottotipo in un tipo antenato, mentre non è possibile fare il contrario, ma è possibile assegnare un oggetto di tipo antenato ad un tipo discendente usando un aggregato che completi i campi mancanti (extension aggregate). L'estensione S di un tipo tagged T tramite la keyword new eredita anche le operazioni primitive per T, ovvero quelle subroutine dichiarate nello stesso package in cui è dichiarato T e che abbiano un parametro o un risultato di tipo T. I tipi derivati hanno quindi un'interfaccia che è sempre un sovrainsieme di quella del tipo da cui derivano, e l'implementazione effettiva delle operazioni ereditate può essere modificata tramite override (l'indicazione esplicita di override nella dichiarazione dell'operazione tramite la keyword overriding, o viceversa di non override con not overriding, è facoltativa ma costituisce un utile controllo statico a tempo di compilazione). Le funzioni di un tipo tagged possono essere richiamate con la dot notation sulla variabile dell'oggetto. L'incapsulamento dello stato interno degli oggetti non è diverso da quello per i tipi non tagged, ed è ottenuto usando il meccanismo di incapsulamento dei package. L'estensione di un tipo può avvenire anche privatamente, per cui i campi aggiunti nell'estensione non sono visibili all'esterno del package.

package Persone is
  type Persona is tagged -- tipo tagged
    record
      Nome    : String;
      Cognome : String;
      Età     : Natural;
    end record;
  function Salario(P: Persona) return Float; -- operazione primitiva, restituisce zero come default
  
  type Lavoratore is new Persona with  -- tipo derivato aggiungendo nuovi campi
    record
      Mansione  : Job;    -- un qualche tipo enumerativo
      Anzianità : Natural;
    end record;
  overriding
  function Salario(L: Lavoratore) return Float; -- override di un'operazione primitiva

  type Studente is new Persona with
    record
      Istituto : School; -- tipo enumerativo
      Corso    : Year;   -- altro tipo enumerativo
    end record;
  -- Studente eredita la funzione Salario di Persona, che restituisce zero

  -- estensione privata
  type Dottorando is new Studente with private;

  -- dichiarazione di un oggetto Studente
  S: Studente := ("John", "Doe", 20, "Trinity College", III);

  -- conversione da un sottotipo ad un tipo antenato
  P: Persona := Persona(S);

  -- assegnamento ad un sottotipo con un extension aggregate
  T: Studente := (P with Istituto => "MIT", Corso => IV);

private
  type Dottorando is new Studente with
    record
      -- i campi aggiunti non sono visibili fuori dal package
      Dipartimento: Department;
    end record;
end Persone;

Dynamic dispatch 
Con il termine "classe" in Ada si indica un insieme di tipi (class wide), costituito da un tipo tagged e da tutti i tipi da esso derivati direttamente o indirettamente. È possibile definire operazioni che hanno parametri o risultato di un tipo class wide usando l'attributo 'Class, ad esempio per un tipo T il suo tipo class wide viene indicato con T'Class. La differenza tra un'operazione con un parametro di tipo T e uno di tipo T'Class è che nel primo caso la scelta della routine da eseguire è determinata staticamente a tempo di compilazione, nel secondo caso è determinata dinamicamente a runtime (dynamic dispatch). Se nell'esempio precedente si aggiunge al tipo Persona la seguente operazione primitiva

function Reddito_Annuo(P: Persona) return Float is
  return 12.0 * P.Salario;
end Reddito_Annuo;

si ha che la funzione restituirà sempre zero per tutti gli oggetti, anche dei tipi come Lavoratore che avessero salario non nullo, perché al suo interno viene sempre richiamata staticamente la funzione Salario definita per il tipo Persona, che restituisce zero. Se invece l'operazione è definita come

function Reddito_Annuo(P: Persona'Class) return Float is
  return 12.0 * P.Salario;
end Reddito_Annuo;

il dispatch della funzione Salario avviene dinamicamente e il risultato restituito è quello corretto anche per gli oggetti di tipo Lavoratore.

Tipi astratti e interfacce 
Se un tipo tagged viene dichiarato astratto, tramite la keyword abstract, non è possibile dichiarare variabili di quel tipo, ma solo usarlo come base da cui derivare altri tipi. I tipi astratti possono avere componenti e routine concrete, ma anche routine a loro volta definite come astratte, che non sono provviste di implementazione. Quando si deriva un tipo concreto da un tipo astratto, tutte le sue routine astratte devono necessariamente essere oggetto di override. Un'interfaccia è un tipo dichiarato con la keyword interface, ed è analogo ad un tipo astratto ma ha maggiori restrizioni, in quanto non può avere componenti né routine concrete, salvo procedure nulle o routine con parametri class wide.

Ada ha un meccanismo di ereditarietà singola per le implementazioni e multipla per le interfacce, simile al Java, per cui è possibile definire tipi che estendono al massimo un tipo concreto o astratto, ma allo stesso tempo possono implementare un numero arbitrario di interfacce. Questo previene possibili conflitti o ambiguità derivanti dall'ereditarietà multipla completa, ma conserva comunque la flessibilità consentendo ad un tipo di poter avere più interfacce di routine. Le interfacce possono essere usate anche con estensioni private, ma in quel caso la vista completa e quella parziale del tipo devono essere conformi rispetto alle interfacce implementate, per cui non è possibile aggiungere o togliere interfacce nel completamento privato della dichiarazione.

package P is
  -- tipo astratto: può avere campi, procedure concrete e astratte
  type S is abstract tagged
    record
      ...
    end record;
  procedure Foo(X: S);
  procedure Baz(X: S) is abstract;

  -- interfaccia: non può avere campi né operazioni concrete (che non 
  -- siano nulle o con parametro class wide)
  type T is interface;
  procedure Foo(X: T) is abstract;
  procedure Baz(X: T) is null;
  procedure Bar(X: T'Class);
end P;

Tipi controllati 
In Ada non esiste il concetto di costruttore, ma è possibile sostituirne le caratteristiche funzionali usando un tipo controllato. Un tipo controllato è un tipo che estende Ada.Finalization.Controlled (oppure Ada.Finalization.Limited_Controlled per i tipi controllati e limitati), per il quale è possibile eseguire l'override di tre procedure (due nel caso dei tipi limitati, dove manca la procedura Adjust):

with Ada.Finalization;
package P is
  type T is new Ada.Finalization.Controlled with
    record
      ...
    end record;

  overriding procedure Initialize (This: in out T);
  overriding procedure Adjust     (This: in out T);
  overriding procedure Finalize   (This: in out T);
end P;

La procedura Initialize viene eseguita sull'oggetto subito dopo la creazione e può svolgere le funzionalità di inizializzazione tipicamente delegate ad un costruttore, la procedura Adjust viene eseguita subito dopo un'assegnazione (per cui non è disponibile per i tipi Limited_Controlled) e può fungere da costruttore di copia, mentre la procedura Finalize viene eseguita immediatamente prima della deallocazione di un oggetto, e funge da distruttore.

Per ragioni storiche, i tipi Ada.Finalization.Controlled e Ada.Finalization.Limited_Controlled non sono interfacce (aggiunte solo in Ada 2005) ma tipi astratti, per cui non è possibile definire un tipo che sia controllato e che contemporaneamente erediti l'implementazione di un tipo non controllato.

Note

Bibliografia

Inglese

Altri progetti

Collegamenti esterni 
 
 
 
 
 
  Forum