Ada (linguaggio di programmazione)

Ada
linguaggio di programmazione
Autore
Data di origine1980
Utilizzogeneral purpose
Paradigmi
Tipizzazione
Estensioni comuni.adb .ads
Influenzato daALGOL 68 (Ada 83), Pascal (Ada 83), C++ (Ada 95), Smalltalk (Ada 95), Java (Ada 2005)
Ha influenzatoC++, Eiffel, PL/SQL, VHDL, Ruby, Java, Seed7
Implementazione di riferimento
Sito webwww.adaic.org

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.[1]

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.[2][3][4]

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

[modifica | modifica wikitesto]

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,[5] 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.[6]

"Hello, world!" in Ada

[modifica | modifica wikitesto]

Un Hello world in Ada è il seguente:[7]

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

A differenza della maggior parte dei linguaggi di programmazione, Ada è insensibile alle maiuscole.[8] 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).[9]

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

[modifica | modifica wikitesto]

Il linguaggio, in riferimento allo standard Ada 2012, ha 73 parole riservate:[10]

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à.[11]

Dichiarazioni di variabile

[modifica | modifica wikitesto]

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.[12]

È 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.[13]

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

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[14] (ad esempio un'iterazione di un ciclo) o l'uscita da cicli profondamente annidati,[15] 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).[16]

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

[modifica | modifica wikitesto]
Gerarchia dei tipi in Ada

Ada è un linguaggio fortemente tipizzato e typesafe.[17] 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)[18] e la conversione di tipo si effettua scrivendo il nome del tipo seguito dalla variabile da convertire tra parentesi[19], 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.[20]

Tipi predefiniti

[modifica | modifica wikitesto]

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.[21]

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:[22]

  • 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:[23]

  • 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

[modifica | modifica wikitesto]

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.[24]

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

[modifica | modifica wikitesto]

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.[25]

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; 

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.[26]

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).[27]

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

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.[29]

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.[30]

-- 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.[31]

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.[32]

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

[modifica | modifica wikitesto]

È 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.[33]

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

[modifica | modifica wikitesto]

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.[34]

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

Ada supporta due tipi di routine, funzioni e procedure, dichiarate con le keyword function e procedure.[35] 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.[36]

-- 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.[37]

-- 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:[38]

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

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.[39]

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).[40]

-- 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.[41]

-- 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; 
I package forniscono un meccanismo di incapsulamento per tipi, variabili e subroutine, permettendo di definire una parte privata che non è accessibile al di fuori del package stesso, dei suoi package annidati e dei suoi figli. È anche possibile definire dei tipi con implementazione privata, che sono visibili utilizzabili al di fuori del package, ma la cui implementazione effettiva non è accessibile al di fuori dello stesso.

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.[42]

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; 

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.[43]

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:[44]

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:[45]

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

[modifica | modifica wikitesto]

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:[46]

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.[47]

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

[modifica | modifica wikitesto]

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.[48]

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

[modifica | modifica wikitesto]

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)[49] 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.[50] 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.[51]

Programmazione orientata agli oggetti

[modifica | modifica wikitesto]

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.[52] 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.

I tipi che riportano tale informazione sono detti tagged (che significa "etichettati"), e viene specificato nella dichiarazione del tipo con l'omonima keyword.[53]

Ereditarietà

[modifica | modifica wikitesto]

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.[53] 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).[54] 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.[55]

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

[modifica | modifica wikitesto]

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).[56] 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.[57]

Tipi astratti e interfacce

[modifica | modifica wikitesto]

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.[58] 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.[59]

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

[modifica | modifica wikitesto]

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,[60] 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.[61]

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.[59]

  1. ^ John Barnes, The Ravenscar profile, su adaic.org.
  2. ^ S. Tucker Taft e Florence Olsen, Ada helps churn out less-buggy code, su gcn.com, Government Computer News, 30 giugno 1999, pp. 2–3. URL consultato il 14 settembre 2010 (archiviato il 31 agosto 2015).
  3. ^ Michael Feldman, Who's using Ada?, su seas.gwu.edu, SIGAda Education Working Group (archiviato il 31 agosto 2015).
  4. ^ Pulling strings 220 miles above Earth - The ISS software serves as the orbiting lab's central nervous system (PDF), Boeing (archiviato dall'url originale il 23 aprile 2015).
  5. ^ Barnes (2014), p. 380.
  6. ^ Gary Dismukes, Gem #63: The Effect of Pragma Suppress, su adacore.com (archiviato il 28 luglio 2015).
  7. ^ Il programma, salvato nel file hello.adb, può essere compilato usando il compilatore GNAT con il comando gnatmake hello.adb
  8. ^ Fanno eccezione i letterali di tipo carattere o stringa.
  9. ^ Barnes (2014), p. 67.
  10. ^ Barnes (2014), p. 851.
  11. ^ Barnes (2014), p. 68.
  12. ^ Barnes (2014), pp. 73-74.
  13. ^ Barnes (2014), p. 75.
  14. ^ La label come ultima riga del blocco è valida solo a partire dallo standard Ada 2012, mentre le versioni precedenti richiedevano che il goto fosse seguito da un'istruzione (quindi bisognava aggiungere un'istruzione nulla al termine del blocco, dopo la label).
  15. ^ Barnes (2014), p. 114.
  16. ^ Peter Van der Linden, Expert C Programming: Deep C Secrets, prentice Hall Professional, 1994, pp. 36-38, ISBN 978-0-13-177429-2.
  17. ^ Barnes (2014), p. 11.
  18. ^ Barnes (2014), p. 211.
  19. ^ Barnes (2014), p. 83.
  20. ^ Barnes (2014), p. 87.
  21. ^ Barnes (2014), p. 18.
  22. ^ Taft et al., pp. 356-359.
  23. ^ Taft et al., pp. 319-322.
  24. ^ Barnes (2014), pp. 77-79.
  25. ^ Barnes (2014), pp. 87-89.
  26. ^ Barnes (2014), p. 117.
  27. ^ Barnes (2014), p. 112.
  28. ^ Barnes (2014), p. 137.
  29. ^ Barnes (2014), pp. 118, 135-136.
  30. ^ Barnes (2014), p. 128.
  31. ^ Barnes (2014), p. 138.
  32. ^ Barnes (2014), pp. 143-146.
  33. ^ Barnes (2014), pp. 439 ss.
  34. ^ Barnes (2014), p. 251.
  35. ^ In questa voce si usano distintamente i tre termini, con significato differente: "funzione" per indicare un sottoprogramma che ha un valore di ritorno, "procedura" per indicare un sottoprogramma che non ha un valore di ritorno, "routine" per indicare un generico sottoprogramma (procedura o funzione).
  36. ^ Barnes (2014), pp. 180 ss.
  37. ^ Barnes (2014), pp. 181-182, 185.
  38. ^ Barnes (2014), p. 169.
  39. ^ Barnes (2014), pp. 265-266.
  40. ^ Barnes (2014), p. 183.
  41. ^ Barnes (2014), pp. 272-273.
  42. ^ Barnes (2014), p. 277.
  43. ^ Barnes (2014), pp. 469 ss.
  44. ^ Barnes (2014), p. 470.
  45. ^ Barnes (2014), p. 471.
  46. ^ Barnes (2014), p. 475.
  47. ^ Barnes (2014), p. 477.
  48. ^ Barnes (2014), pp. 485-491.
  49. ^ Bruce Powel Douglass, Doing Hard Time: Developing Real-time Systems with UML, Objects, Frameworks, and Patterns, Addison-Wesley, 1999, p. 91, ISBN 978-0-201-49837-0.
  50. ^ Barnes (2014), p. 787.
  51. ^ C.K.W. Grein, Preventing Deallocation for Reference-counted Types, su adacore.com, AdaCore (archiviato il 31 luglio 2015).
  52. ^ Barnes (2014), p. 30.
  53. ^ a b Barnes (2014), p. 31.
  54. ^ Barnes (2014), p. 306.
  55. ^ Barnes (2014), p. 334.
  56. ^ Barnes (2014), pp. 34-35.
  57. ^ Barnes (2014), p. 35.
  58. ^ Barnes (2014), pp. 347-348.
  59. ^ a b Barnes (2014), p. 350.
  60. ^ Tecnicamente non si tratta di un costruttore, che viene eseguito durante la creazione dell'oggetto e dopo l'esecuzione dei costruttori di eventuali superclassi.
  61. ^ Barnes (2014), pp. 342-346.

Altri progetti

[modifica | modifica wikitesto]

Collegamenti esterni

[modifica | modifica wikitesto]
Controllo di autoritàLCCN (ENsh85000774 · GND (DE4000430-2 · BNE (ESXX531014 (data) · J9U (ENHE987007293846605171
  Portale Informatica: accedi alle voci di Wikipedia che trattano di informatica