LINGUAGGI PROGRAMMATIVI
I l. p. (o linguaggi di programmazione) costituiscono il mezzo linguistico tramite cui gli elaboratori possono essere programmati. Essi costituiscono pertanto lo strumento principale per la comunicazione tra chi progetta programmi per la risoluzione automatica di problemi e gli elaboratori in grado di eseguire i programmi stessi (v. anche elaboratori elettronici: Linguaggi programmativi, App. IV, i, p. 652). Per la precisione, gli elaboratori sono in grado di eseguire direttamente soltanto programmi scritti nel proprio linguaggio macchina. Tale linguaggio risulta però di difficile impiego da parte dell'uomo: non usa concetti propri della comunicazione umana, fa riferimento ai dettagli del funzionamento dell'hardware e risulta del tutto innaturale. I l. p. forniscono pertanto uno strumento di comunicazione linguistica più orientato alle esigenze espressive dell'uomo, e quindi più adatto alla scrittura di programmi di facile comprensione. Poiché gli elaboratori sono in grado di eseguire direttamente soltanto i programmi scritti nel proprio linguaggio macchina, è necessario disporre di strumenti appositi per rendere eseguibili i l. p.; tali strumenti ricadono in due categorie: i compilatori e gli interpreti. Gli interpreti sono programmi che ricevono come dati d'ingresso sia il programma da interpretare che i valori sui quali il programma deve operare; essi generano le azioni necessarie per eseguire il programma sui suoi dati. I compilatori sono invece dei traduttori dal linguaggio di programmazione (detto anche linguaggio sorgente) al linguaggio macchina (detto anche linguaggio oggetto).
Sintassi e semantica. − I l. p. sono definiti attraverso la specificazione della sintassi e della semantica. La sintassi fornisce le regole di formazione di programmi testualmente corretti; in base a essa è così possibile discriminare i testi che non sono costruiti correttamente, e che pertanto non corrispondono a programmi, dai testi che sono ben formati e che costituiscono programmi del linguaggio. La sintassi di un linguaggio può essere definita in vari modi; tra questi è particolarmente diffusa la cosiddetta forma normale di Backus-Naur (BNF), di cui peraltro esistono diverse varianti.
Un esempio di descrizione sintattica in BNF è il seguente, che definisce la sintassi delle espressioni di un linguaggio:
La grammatica precedente s'interpreta così: il simbolo :=separa la parte sinistra della regola, nella quale appare la categoria sintattica che si sta definendo (racchiusa tra parentesi angolari), dalla parte destra, nella quale appare una sequenza di simboli del linguaggio (racchiusi tra apici) e di categorie sintattiche; la parte racchiusa tra parentesi quadre è da intendersi come opzionale, mentre il simbolo di barra verticale separa diverse alternative di definizione della categoria sintattica che sta a sinistra di :=. Per es., la prima regola si legge: un'espressione è costituita da un termine, preceduto opzionalmente da un'espressione seguita da un operatore che può essere +o− .
Al fine di dimostrare che una sequenza di caratteri costituisce un'espressione secondo la precedente sintassi, è possibile procedere in modo sistematico effettuando l'analisi sintattica della sequenza. Per es., nel caso della sequenza
(a+b)*(a+b*c)
l'analisi sintattica può essere sintetizzata nel modo seguente:
(a+b)*(a+b*c) è un'espressione perché essa è un termine;
(a+b)*(a+b*c) è un termine perché (a+b) è un termine, è un op2 e (a+b*c) è un fattore;
(a+b) è un termine perché esso è un fattore;
(a+b) è un fattore perché a+b è un'espressione racchiusa tra parentesi tonde;
a+b è un'espressione perché "......";
(a+b*c) è un fattore perché a+b*c è un'espressione racchiusa tra parentesi tonde;
a+b*c è un'espressione perché a è un'espressione, b è un op1 e b*c è un termine;
a è un'espressione perché ".....";
b*c è un termine perché "...."
Con "..." abbiamo indicato parti di dimostrazione che dovrebbero essere ulteriormente dettagliate, ma il cui sviluppo è ovvio.
La sintassi di un l. p. dà soltanto regole per la buona formazione dei programmi, ma non impedisce che vengano prodotti programmi che non hanno un significato. La definizione del significato di un linguaggio (semantica) è pertanto composta da due parti: la prima (semantica statica) discrimina tra i programmi sintatticamente corretti quelli che, in base a un esame del testo, non hanno significato; la seconda (semantica dinamica) descrive invece il significato delle diverse istruzioni del linguaggio, specificando che cosa avviene durante la loro esecuzione. La semantica statica di un linguaggio impone per es. che i dati ai quali si accede all'interno di un'espressione aritmetica abbiano un tipo numerico, vietando pertanto che si sommi una variabile intera a un carattere o un booleano. La semantica dinamica, nel caso dell'espressione precedente, ci dice che prima si valutano a+b (il cui risultato denotiamo con t1) e b*c (il cui risultato denotiamo con t2); successivamente si valuta a+t2 (il cui risultato denotiamo con t3) e infine il risultato dell'espressione si valuta come t1*t3.
Mentre la sintassi di un l. p. è facile da definire ed è prassi ormai consueta che essa venga definita in maniera formale, per es. ricorrendo alla BNF, la semantica è a tutt'oggi definita in modo informale, attraverso spiegazioni intuitive in linguaggio naturale. Esistono, tuttavia, metodi rigorosi o addirittura formali per definire la semantica dei linguaggi, che possono essere classificati in due categorie: metodi operazionali e metodi denotazionali. Un metodo operazionale definisce la semantica di un linguaggio descrivendo come una macchina astratta opererebbe per eseguire le diverse istruzioni del linguaggio. Un metodo denotazionale definisce invece il significato di ogni costrutto del linguaggio, e quindi di un intero programma, fornendo una funzione matematica, la cui valutazione produrrebbe gli stessi risultati dell'esecuzione del programma sugli stessi dati.
I linguaggi convenzionali (o di von Neumann). − La struttura degli elaboratori tradizionali si rifà al modello di von Neumann (v. elaboratori elettronici, in questa Appendice). Un grande numero di l. p., partendo dalla struttura di base di von Neumann, fornisce delle astrazioni che ignorano molti dei dettagli di funzionamento della macchina, al fine di fornire uno strumento linguistico più orientato all'uso da parte dell'uomo. Tali l. p. vengono pertanto chiamati linguaggi di von Neumann (lvn). Maggiore risulta il livello di astrazione rispetto alla sottostante architettura di von Neumann e maggiore si definisce il livello del linguaggio.
Un'importante classe di astrazioni fornita da un lvn riguarda i dati e la loro manipolazione. Tipicamente, in linguaggio macchina, i dati sono accessibili in base all'indirizzo delle celle di memoria che li contengono e il contenuto di tali celle può essere modificato mediante istruzioni di assegnamento. Questi concetti si riflettono in tutti i l. p., nei quali le variabili costituiscono dei contenitori modificabili di valori. I linguaggi assemblativi costituiscono un primo livello di astrazione rispetto al linguaggio macchina, in quanto consentono di esprimere in modo simbolico le istruzioni del linguaggio macchina. Innanzitutto, il codice delle operazioni da effettuare, anziché essere espresso come sequenza di caratteri binari, risulta espresso in modo simbolico: per es., il codice ADD potrebbe essere usato per esprimere l'operazione di addizione tra un dato contenuto in una certa cella di memoria e un altro dato contenuto in un certo registro dell'unità operativa. Inoltre, anziché denotare un dato in base all'indirizzo fisico della cella di memoria che lo contiene, viene usato un nome simbolico. Il nome simbolico, detto comunemente variabile, costituisce a tutti gli effetti un indirizzo simbolico per il dato.
Per es., ADD LATO denota l'operazione di somma del valore della variabile LATO (il contenuto della cella d'indirizzo simbolico LATO) al contenuto di un certo registro dell'unità operativa, con risultato prodotto in detto registro. Il seguente frammento in un ipotetico linguaggio assemblativo descrive pertanto il calcolo del perimetro di un rettangolo i cui lati hanno una dimensione memorizzata nelle variabili LATO1 e LATO2:
LOAD LATO1
ADD LATO2
MULT 2
STORE PERIM
dove s'ipotizza che LOAD LATO1 carichi il valore della variabile LATO1 nel registro, MULT sia il codice dell'operazione di moltiplicazione e STORE PERIM trasferisce il valore dal registro dell'unità operativa alla cella di indirizzo simbolico PERIM.
I linguaggi assemblativi sono lvn di basso livello, in quanto forniscono una limitata astrazione dalla struttura di von Neumann, essenzialmente limitata all'uso di nomi simbolici anziché codici numerici. Pur essendo limitata, comunque, l'astrazione fornisce indubbi vantaggi. Innanzitutto si ottiene un miglioramento della comprensibilità dei programmi, dovuta alla scelta dei nomi simbolici, che possono essere selezionati in modo da evocare il reale significato delle operazioni e dei dati. Inoltre, si ottiene una maggiore facilità di modifica dei programmi, in quanto non ci si riferisce agli indirizzi fisici dei dati, ma al loro identificatore simbolico. Qualora l'aggiunta (o eliminazione) di nuovi dati o istruzioni alterasse l'indirizzo fisico dei dati, il programma che usa identificatori simbolici non ne sarebbe influenzato.
Di gran lunga più diffusi nella programmazione sono oggi i linguaggi di alto livello, nei quali si realizzano astrazioni sempre più marcate rispetto alla struttura di von Neumann. Quattro sono le direzioni nelle quali i lvn di alto livello si sono sviluppati e in base alle quali possono essere valutati, confrontati e scelti: le astrazioni che forniscono rispetto ai dati, le astrazioni rispetto al controllo, il supporto fornito alla produzione di programmi corretti e facilmente modificabili, e il supporto fornito alla programmazione di grossi sistemi software.
Astrazioni fornite rispetto ai dati. - I linguaggi macchina e i linguaggi assemblativi vedono i dati come sequenze binarie, manipolabili in quanto tali e indipendentemente dalle grandezze che essi rappresentano. Per es., se una certa variabile denota una lunghezza, il suo valore può essere manipolato mediante operazioni sulla sequenza di bit, quali la complementazione bit a bit o lo scorrimento della sequenza a destra o a sinistra, anche se a queste operazioni non appare facile attribuire un significato logico in quanto operazioni su una lunghezza. I linguaggi di alto livello introducono il concetto di tipo di dato per caratterizzare la classe di valori che una certa variabile può assumere legalmente. Per es., si può dichiarare una variabile di tipo integer, nel qual caso essa è correttamente manipolabile dalle operazioni numeriche sugli interi, mentre si genera una condizione di errore nel programma se si cerca di operare sulla variabile mediante le operazioni, quali and, or o not, associate ai dati di tipo boolean.
I l. p. mettono a disposizione un insieme predefinito di tipi, che si suppone soddisfino la gran parte delle necessità dei programmatori; tipicamente, i tipi integer, real, boolean, character, dall'ovvio significato. Accanto a essi, offrono costruttori di aggregati, quali array, record, file, mediante cui è possibile strutturare dati complessi. Il costruttore array permette di definire collezioni di dati dello stesso tipo, i cui componenti sono individualizzabili univocamente in base ai loro indici, cioè la loro posizione nell'array. Il costruttore record permette di aggregare componenti di tipi potenzialmente disomogenei e di accedere univocamente a tali componenti specificando il nome di un selettore, detto campo. Il costruttore file permette di definire archivi di dati, e cioè sequenze di lunghezza a priori illimitata di elementi dello stesso tipo, i cui componenti sono resi accessibili mediante una scansione, spesso sequenziale, dell'archivio stesso.
Per es., usando la sintassi del linguaggio Pascal, volendo definire una tabella che memorizza i dati anagrafici dei 20 allievi di una classe, è possibile definire la seguente variabile:
var CLASSE: array [1:20] of
record
NOME,COGNOME:array[1:20]ofcharacter;
ANNO_NASCITA: 1900:2000;
end;
Ogni elemento della variabile classe è individuato da un indice compreso fra 1 e 20; volendo quindi accedere a un elemento, per es. quello di indice 7, si scrive CLASSE [7]. L'anno di nascita dell'allievo di indice 7 viene denotato da CLASSE [7].ANNO_NASCITA; la prima lettera del suo nome è invece denotata da CLASSE [7].ANNO_NOME [1].
L'esempio precedente mette in evidenza un'ulteriore caratteristica di alcuni l. p., e cioè la possibilità di definire dei campi limitati di variabilità (subrange) di certe variabili. In particolare, il valore contenuto nel campo ANNO_NASCITA può assumere solo valori interi compresi nell'intervallo chiuso 1900...2000. Oltre alle caratteristiche fin qui illustrate per definire astrazioni sui dati, alcuni l. p. permettono di definire nuovi tipi (detti tipi concreti), di cui possono essere dichiarate diverse variabili. Per es., ipotizzando che si debbano poter trattare nel programma diverse classi, e che queste possano avere un numero massimo di studenti non superiore a 30, si potrebbe dichiarare (sempre in Pascal) il seguente tipo:
type CLASSE:
record
N_STUD: 0:30;
ALLIEVI: array [1:30] of
record
NOME, COGNOME: array [1:20] of character;
ANNO_NASCITA: 1900:2000
end
end;
Diverse variabili del tipo CLASSE potrebbero essere dichiarate come:
var PRIMA_A, SECONDA_B, TERZA_C: CLASSE;
il campo N_STUD di tali variabili (per es., PRIMA_A.N_STUD) denota il numero di studenti di tale classe, che non può superare il valore costante 30, e che denota la porzione (delimitata da indici 1 e PRIMA_A.N_STUD) dell'array PRIMA_A che contiene le informazioni relative agli allievi della classe. I nuovi tipi definiti secondo queste modalità si differenziano dai tipi predefiniti del linguaggio per un minore livello di astrazione. Infatti i tipi predefiniti mascherano al programmatore in modo totale la rappresentazione dei tipi nella memoria dell'elaboratore. Per es., il programmatore ignora se variabili del tipo intero siano memorizzate in un singolo byte, in una coppia di bytes, o in altra struttura concreta; egli non può pertanto accedere alla struttura di memorizzazione, né modificarne il valore. Le sole possibilità di accedere e modificare le variabili sono quelle offerte dalle operazioni definite dal linguaggio per manipolare gli interi (somme, sottrazioni, moltiplicazioni, divisioni, ecc.). Viceversa, la definizione di un tipo concreto fornisce semplicemente un nome a una struttura di dati, ma non fornisce alcuna operazione per la manipolazione delle variabili del tipo concreto. Tali variabili risultanto manipolabili solo accedendo alla rappresentazione del dato. Per es., per iscrivere l'allievo X a una classe C, supposto che questa non sia già completa, occorre scrivere le seguenti operazioni:
C.N_STUD:=C.N_STUD+1;
C.ALLIEVI[C.N_STUD]:=X;
I tipi concreti consentono di fattorizzare in un solo punto del programma la definizione di strutture di dati di cui si devono poi generare diversi esemplari; tuttavia forniscono una capacità di astrazione sui dati molto limitata. Alcuni linguaggi offrono la possibilità di definire tipi di dati astratti, vale a dire tipi di dati definiti dal programmatore, per i quali sono pure definite le operazioni astratte mediante le quali le variabili del tipo possono essere manipolate, e al tempo stesso sono invece inaccessibili i dettagli relativi alla modalità di rappresentazione dei dati.
Proseguendo nell'esempio precedente, si supponga di voler definire il tipo di dato astratto CLASSE, sul quale si può agire mediante un'operazione ISCRIVI, che iscrive un nuovo allievo alla classe, un'operazione QUANTI_SONO che fornisce il numero di iscritti alla classe, e un'operazione STAMPA_ELENCO, che stampa l'elenco degli iscritti alla classe nell'ordine alfabetico. Un ipotetico lvn che fornisca la possibilità di definire tipi di dati astratti potrebbe consentire la definizione:
abstract type CLASSE=
operations ISCRIVI(in X: ALLIEVO; in out C: CLASSE);
STAMPA_ELENCO (in C: CLASSE);
QUANTI_SONO (in C: CLASSE): INTEGER
implementation
type CLASSE:
record
N_STUD: 0:30;
ALLIEVI: array [1:30] of ALLIEVO;
end;
procedure ISCRIVI (in X: ALLIEVO; in out C: CLASSE);
algoritmo per la realizzazione dell'operazione, che riceve come parametri X (parametro d'ingresso) e C (parametro sia d'ingresso che di uscita) ed effettua l'inserimento del valore di X (i dati anagrafici di un allievo). Si osservi che il tipo ALLIEVO non è qui dettagliato per semplicità
procedure STAMPA_ELENCO (in C: CLASSE);
algoritmo per la realizzazione dell'operazione
function QUANTI_SONO (in C: CLASSE): INTEGER;
algoritmo per la realizzazione dell'operazione
end;
Il tipo di dato astratto permette di definire astrazioni sui dati che si comportano esattamente come le astrazioni fornite dai tipi predefiniti: vengono fornite delle apposite operazioni per operare sui dati del nuovo tipo e al tempo stesso vengono rese inaccessibili le informazioni relative alla realizzazione (descritta nella sezione implementation). Con ciò il programmatore può estendere il lvn di partenza definendo, accanto ai tipi predefiniti, i nuovi tipi di dati che si rendono necessari per meglio caratterizzare l'applicazione che sta progettando. Oltre al vantaggio di ottenere in questa maniera un maggiore livello di astrazione dalla sottostante struttura dell'elaboratore, si ottiene anche il fondamentale vantaggio di poter poi modificare la realizzazione del tipo astratto senza che il programma che lo usa ne risulti influenzato. Per es., si supponga di voler passare da una realizzazione che ordina la tabella degli iscritti solo quando viene richiesta l'operazione di stampa dell'elenco a una realizzazione che mantiene la tabella ordinata dopo ciascun inserimento. Questa decisione, che potrebbe essere dettata da motivi di velocità di risposta all'operazione di stampa dell'elenco, non verrebbe a influenzare il programma, ma resterebbe confinata all'interno della sezione implementation del tipo di dato astratto.
Astrazioni fornite rispetto al controllo. − Il flusso di controllo all'interno di un programma in linguaggio macchina è molto primitivo; essenzialmente, le istruzioni vengono eseguite in sequenza (e cioè secondo valori crescenti degli indirizzi delle celle in cui sono memorizzate) e al più si può alterare il flusso sequenziale mediante istruzioni di salto, che trasferiscono il controllo in modo esplicito a una certa istruzione memorizzata a un certo indirizzo. La stessa struttura logica si ritrova nei linguaggi assemblativi, i quali forniscono in più la possibilità di astrarre dagli indirizzi fisici delle istruzioni, specificando indirizzi simbolici. Accanto a ciò, i linguaggi assemblativi evoluti offrono la possibilità di specificare sottoprogrammi (v. oltre).
I linguaggi di alto livello offrono numerosi schemi nei quali si può organizzare il flusso del controllo tra le istruzioni. Oltre alla sequenza, che corrisponde al meccanismo elementare fornito dallo hardware, e che spesso viene denotata come
S1; S2; ...Sn
dove S1, S2, ...Sn sono istruzioni del linguaggio, i lvn di alto livello forniscono strutture di controllo per denotare la selezione fra diverse alternative di esecuzione e l'iterazione di certe istruzioni.
La più diffusa struttura di controllo per selezionare alternative è la cosiddetta istruzione ''if'':
if COND then
S1
else
S2
end if;
secondo la quale viene eseguita S1 se il valore della condizione COND è vero, mentre viene eseguita S2 se tale valore è falso.
La più diffusa struttura iterativa è il cosiddetto ''ciclo while'':
while COND do
S
end do;
secondo cui l'esecuzione di S viene iterata finché la condizione COND resta vera.
Accanto a queste strutture di controllo, vengono spesso definite anche altre strutture, allo scopo di rendere il linguaggio più agile e facile da usare da parte dell'uomo. Per es., una comune struttura selettiva è la seguente:
case expression of
X1:S1;
X2:S2;
Xn:Sn;
otherwise
S
end case;
In base a questa struttura di controllo, a seconda che il valore dell'espressione ''expression'' sia X1 oppure X2, ...oppure Xn si esegue l'istruzione S1, oppure S2, ...oppure Sn; se il valore non coincide con nessuno di quelli elencati esplicitamente, si esegue S.
Una comune struttura iterativa è invece la seguente:
for i=a. .b do
S
end do;
Questa struttura di controllo specifica l'iterazione dell'istruzione S per i che assume tutti i valori nell'intervallo chiuso a. .b.
Le astrazioni sul controllo che abbiamo illustrato in precedenza descrivono come il controllo passa durante l'esecuzione tra le diverse istruzioni del programma; tali astrazioni vengono anche dette strutture di controllo in piccolo. Definiamo invece strutture di controllo in grande i costrutti forniti dal linguaggio di programmazione per specificare il flusso di controllo tra le diverse unità che compongono un programma. Intuitivamente e informalmente, un'unità può essere vista come un costituente di un sistema software complesso; essa può costituire un'unità di lavoro, la cui progettazione, programmazione e verifica può essere anche affidata a una persona.
Tra le strutture di controllo in grande, il caso più diffuso è quello delle chiamate a sottoprogramma. Un sottoprogramma costituisce una operazione astratta, che viene definita separatamente dal programma e che da questo può essere esplicitamente chiamata; la chiamata specifica i parametri sui quali il sottoprogramma deve operare. Quando il sottoprogramma termina, il controllo viene automaticamente e implicitamente trasferito all'unità chiamante, al punto in cui la chiamata era stata in precedenza effettuata. Accanto a questo meccanismo, alcuni l. p. forniscono strutture di controllo per la gestione di eventi anomali (exception handling). Gli eventi anomali consistono in malfunzionamenti che vengono rilevati durante l'esecuzione e il cui rilevamento comporta un automatico e implicito trasferimento del controllo a opportune unità, dette gestori delle anomalie. Una gestione di anomalia potrebbe per es. essere progettata per essere attivata quando durante l'esecuzione si cerca di effettuare una divisione per zero, o si ha un superamento dell'intervallo di variabilità dell'indice di un array. Costrutti linguistici per la gestione delle anomalie sono particolarmente rilevanti per l. p. che si prefiggono di supportare una programmazione altamente affidabile.
Ulteriori strutture di controllo in grande riguardano la programmazione di unità semiconcorrenti e concorrenti. Un'unità semiconcorrente (o coroutine) opera con altre unità semiconcorrenti in modo tale che tutte le unità procedano nell'esecuzione, alternandosi nell'uso dell'unità centrale della macchina. Le unità si cedono esplicitamente il controllo l'un l'altra e, ogni volta che vengono riprese nell'esecuzione, ripartono esattamente dal punto al quale si erano in precedenza interrotte; in tal modo esse simulano un funzionamento ''come se'' tutte le unità procedessero contemporaneamente.
Un'unità concorrente, invece, opera in parallelo con altre unità, almeno da un punto di vista logico; la concorrenza logica diventa concorrenza fisica qualora le diverse attività possano essere eseguite da diversi elaboratori. Un'unità concorrente non deve esplicitamente cedere il controllo ad altre unità, ma deve semplicemente sincronizzarsi con esse. Le astrazioni linguistiche offerte dal l. p. consistono pertanto nel permettere di mandare in esecuzione parallela diverse attività e di sincronizzare le diverse attività per ottenere una corretta cooperazione.
Supporto alla produzione di programmi corretti e facilmente modificabili. − Tra le qualità che deve possedere un prodotto software, particolare rilievo assumono la correttezza (o affidabilità) e la possibilità di apportare modifiche al prodotto, che ne assicurino l'evoluzione futura (v. anche software, in questa Appendice). L'esigenza di correttezza si giustifica col fatto che il software viene sempre più impiegato in applicazioni critiche, nelle quali l'effetto dei malfunzionamenti può avere conseguenze disastrose da un punto di vista economico o civile e può addirittura mettere in pericolo la vita delle persone. L'esigenza di produrre programmi facilmente modificabili trova invece la sua giustificazione nella constatazione che i costi prevalenti del software risiedono negli interventi che vengono su di esso apportati dopo il rilascio dell'applicazione, globalmente denominati col termine manutenzione.
Un l. p. può influenzare la correttezza dei programmi in due modi. Innanzitutto, permettendo di effettuare sui programmi delle verifiche che assicurino l'assenza di certi errori; inoltre, imponendo una disciplina di programmazione che aiuti chi sviluppa l'applicazione a operare avendo sempre sotto controllo la complessità e quindi impedendo che si commettano errori. La prima categoria include le verifiche di correttezza sintattica e di semantica statica; essa è tanto più efficace, quanto più stringenti sono le regole di correttezza semantica statica specificate dal linguaggio.
Molti l. p. moderni impongono la cosiddetta tipizzazione forte dei dati, intendendosi con ciò che ogni variabile debba essere dichiarata specificando il tipo a cui appartiene, e che la verifica che ogni variabile sia manipolata in modo coerente con il proprio tipo debba poter essere fatta staticamente dal compilatore, senza dover mandare il programma in esecuzione. In generale, il fatto di poter verificare staticamente l'eventuale presenza di un errore costituisce un forte ausilio alla produzione di software corretto, in quanto l'eventuale presenza di un errore viene senz'altro rilevata dal compilatore, evitando il pericolo che si rilasci un programma scorretto, in cui l'errore nascosto potrebbe sorgere in un momento imprevisto durante l'esecuzione.
La modificabilità di un programma viene facilitata cercando di localizzare in parti ben delimitate del programma l'effetto dei cambiamenti che devono essere apportati. È chiaro infatti che se ogni cambiamento dovesse richiedere un riesame e una potenziale modifica dell'intero programma il costo del cambiamento sarebbe molto alto e l'affidabilità del risultato sarebbe molto bassa. Un esempio di ciò è stato in precedenza citato nel caso dei tipi di dati astratti, in cui i costrutti del linguaggio di programmazione mascherano al resto del programma l'effetto di un eventuale cambiamento della struttura di dati scelta per realizzare il tipo astratto, localizzando la modifica all'interno del costrutto stesso. Ulteriori commenti su quest'aspetto verranno ripresi nel prossimo punto, in quanto il supporto fornito alla programmazione di grandi sistemi ha lo scopo primario di rendere possibili i cambiamenti, senza che essi abbiano un impatto globale su tutto il sistema.
Supporto alla programmazione di grandi sistemi software. − Molti sistemi software che vengono realizzati a livello industriale hanno un'elevata complessità, che spesso deriva da una notevole dimensione in termini di numero di linee di programma (decine di migliaia, ..., milioni). Lo sviluppo di tali sistemi non può essere demandato a una singola persona, ma richiede la cooperazione di un gruppo di lavoro (v. anche software, in questa Appendice). Lo sviluppo di grossi sistemi richiede che il linguaggio favorisca una scomposizione dell'applicazione in moduli, sviluppabili in modo indipendente da diverse persone e quindi integrabili in un unico sistema in maniera affidabile. Il linguaggio deve anche favorire la produzione di moduli riusabili, sicché la produzione di nuovi sistemi possa progressivamente divenire un'attività di assemblaggio di componenti predefiniti.
Un l. p. può favorire la scomposizione di un sistema in moduli permettendo di specificare in modo chiaro i servizi che ciascun modulo realizza e che vengono esportati, affinché possano essere usati da altri moduli, isolando e rendendo invisibili e inaccessibili agli altri moduli come tali servizi vengano realizzati. In questo modo, la realizzazione può essere cambiata senza che gli altri moduli ne risultino influenzati. Un tipo di dato astratto, come sopra illustrato, costituisce un modulo che esporta agli altri moduli la possibilità di generare esemplari dello stesso tipo e di operare su di essi secondo le operazioni esportate, nascondendo la struttura di dati e gli algoritmi scelti per la realizzazione.
Lo sviluppo di un sistema complesso da parte di più persone è anche favorito dalla possibilità di compilare separatamente i diversi moduli, sicché si possano eliminare gli errori statici e, attraverso l'esecuzione su dati di prova, anche molti degli errori dinamici presenti nel modulo, prima che questo venga integrato con gli altri componenti del sistema. Molti linguaggi consentono una compilazione separata, ma non indipendente, intendendosi con ciò che la compilazione di un modulo verifichi che esso s'interfacci correttamente con gli altri moduli; ciò evita un possibile inconveniente della compilazione separata e indipendente, in cui errori di interfacciamento possano sorgere durante l'esecuzione. Un classico esempio è costituito dalla verifica che la chiamata da parte di un modulo di un sottoprogramma definito in un altro modulo avvenga con parametri consistenti in numero e tipo.
Linguaggi non convenzionali (non di von Neumann). − Linguaggi funzionali. - I linguaggi funzionali differiscono dai linguaggi convenzionali in quanto si basano sui concetti matematici di funzione, piuttosto che sul modo di operare di una macchina di von Neumann. In particolare, il concetto di variabile non è più visto come il modo per denotare un contenitore di valore che può essere modificato durante l'esecuzione passo-passo del programma, ma viene visto in modo matematico come un nome simbolico a cui viene legato un valore per la valutazione della funzione. Scompare pertanto il concetto tipico dei linguaggi di von Neumann secondo il quale la computazione associata a un programma consiste in una sequenza di passi di esecuzione, in cui ogni passo opera un cambiamento nello stato della macchina, depositando nuovi valori o modificando i valori precedentemente memorizzati. In un linguaggio funzionale, una volta che alle variabili vengano legati dei valori, le funzioni vengono valutate, senza che ciò produca un cambiamento di stato.
Il fatto che la valutazione delle funzioni non provochi un cambiamento di stato fa sì che i linguaggi funzionali siano potenzialmente eseguibili da macchine parallele. Per es., dovendo valutare
F (G (X, Y), H (H, Y), Z)
la valutazione di G ed H può avvenire in parallelo.
I linguaggi funzionali si distinguono gli uni dagli altri essenzialmente in base al dominio dei dati che sono gli argomenti delle funzioni; nei casi più diffusi tale dominio è quello delle liste di dati. Il linguaggio fornisce un insieme primitivo di funzioni per manipolare i dati e permette al programmatore di definire nuove funzioni.
Tradizionalmente, i linguaggi funzionali sono estremamente dinamici; per es., non richiedono che il tipo dei dati debba essere dichiarato staticamente e che il corretto uso dei dati debba essere verificato staticamente (tipizzazione debole). Inoltre essi non richiedono che la porzione di programma, in cui un certo dato sia visibile, sia definita staticamente, ma permettono che questa vari dinamicamente durante l'esecuzione. Più recentemente, tuttavia, i linguaggi funzionali si sono andati evolvendo in modo da garantire una maggiore verificabilità statica, al fine di garantire la produzione di software più affidabile.
Linguaggi logici. − Mentre un linguaggio funzionale definisce un programma come una funzione, un programma logico lo definisce come una relazione tra i dati d'ingresso e quelli di uscita, descritta mediante una proprietà espressa usando la logica matematica. Essendo una relazione, agli stessi ingressi possono corrispondere più uscite (o nessuna uscita); l'interprete del linguaggio fornisce pertanto una qualunque delle uscite possibili o, se si preferisce, tutte le uscite possibili (se queste sono in numero finito).
Per es., dovendo descrivere in un ipotetico linguaggio logico l'ordinamento di una sequenza A in una sequenza B, potremmo scrivere
ordina (A, B) if ordinato (B) and permutazione (A, B)
e quindi definire
ordinato (X) if for all i, 1≤i≤#(X) - 1 and Xi≤Xi+1
dove # è l'operatore che dà la lunghezza di una sequenza; il predicato permutazione (X, Y) dovrebbe essere a sua volta definito.
Come si può osservare dall'esempio, lo stile di programmazione logica differisce notevolmente sia dallo stile convenzionale che dallo stile funzionale. La programmazione consiste nel definire proprietà, più che nel definire algoritmi di soluzione o funzioni; in tal senso, si dice anche che essa costituisce una programmazione dichiarativa.
Rassegna dei principali linguaggi di programmazione. − La storia dei l. p. inizia nella seconda metà degli anni Cinquanta e il settore è tuttora in evoluzione. La fig. illustra in modo sintetico le tappe principali di tale evoluzione, mentre qui di seguito forniamo una breve illustrazione dei contributi forniti dai diversi linguaggi.
I primi linguaggi convenzionali di alto livello (FORTRAN, COBOL e ALGOL 60) sono nati a seguito di precise esigenze applicative, e ciascuno di essi ha apportato contributi di fondamentale importanza all'evoluzione dei linguaggi programmativi.
Il FORTRAN (acronimo per FORmula TRANslator) nacque, come dice il nome, per facilitare il compito del programmatore di software matematico nello scrivere formule complesse. Esso divenne subito lo strumento di programmazione più diffuso per il calcolo scientifico, in quanto forniva tipi di dati di natura matematica (interi, reali in semplice e doppia precisione, e complessi), possibilità di aggregazione in matrici di varie dimensioni e possibilità di esprimere calcoli algebrici complessi mediante semplici espressioni. Accanto a ciò, il FORTRAN introdusse per primo la possibilità di sviluppare e compilare separatamente moduli, anche se il concetto di modulo supportato era alquanto primitivo (limitandosi ai soli sottoprogrammi di tipo procedurale) e la compilazione separata e indipendente non permetteva di rilevare eventuali errori d'interfacciamento tra diversi moduli.
Il COBOL (acronimo per COmmon Business Oriented Language) nacque invece come linguaggio di supporto per la scrittura di applicazioni gestionali, nelle quali gli aspetti di ''calcolo'' sono usualmente piuttosto semplici, mentre invece risultano prevalenti gli aspetti di gestione di archivi permanenti di informazioni. Il COBOL permette pertanto di definire e manipolare files, che rappresentano strutture informative permanenti su memoria secondaria (dischi, nastri, ecc.). Mentre FORTRAN e COBOL sono linguaggi tuttora molto utilizzati, l'ALGOL 60 è un linguaggio ormai in disuso, anche se storicamente ha il merito di aver introdotto novità di grande rilievo. Innanzitutto il linguaggio si caratterizza per una notevole eleganza matematica; inoltre ha introdotto per primo i concetti di struttura a blocchi e di ricorsione, che si ritrovano in molti linguaggi moderni. La struttura a blocchi consente di organizzare in maniera gerarchica all'interno di un programma la visibilità dei nomi dichiarati; la ricorsione consente di definire sottoprogrammi che chiamano se stessi, eventualmente in modo indiretto.
Nello stesso periodo in cui venivano definiti i primi linguaggi convenzionali di alto livello, iniziava la sperimentazione con formalismi linguistici non convenzionali. Ovviamente la relativa inefficienza delle architetture degli anni Cinquanta rispetto alle architetture attuali e il fatto che i linguaggi in questione fossero progettati senza prestare attenzione all'architettura dell'elaboratore sottostante fecero sì che le prime realizzazioni dei linguaggi non convenzionali fornissero un'efficienza inaccettabile in pratica. Ciò costituì il motivo principale per cui i linguaggi non convenzionali non ebbero una diffusione nel mondo applicativo, ma rimasero confinati all'interno del mondo della ricerca e presto diedero luogo a numerosi dialetti ed estensioni che impedirono, fino agli anni recenti, una reale standardizzazione.
Il più noto è il LISP, un linguaggio funzionale per la manipolazione di liste che risulta particolarmente efficace a programmare applicazioni di elaborazione simbolica. Negli anni recenti ha riscosso un successo crescente anche in ambiti industriali, grazie alla sua standardizzazione (il cosiddetto Common LISP) e al diffondersi di applicazioni quali i cosiddetti sistemi esperti, programmi che accumulano l'esperienza di specifici settori applicativi e consentono di risolvere alcuni problemi specifici del settore utilizzando la conoscenza accumulata.
L'APL, che ha avuto una certa diffusione applicativa, si presenta come un linguaggio che permette di manipolare matrici. Lo SNOBOL4, di più limitata diffusione, consente invece di manipolare testi.
Il PL/I nasce come primo tentativo di linguaggio universale, e ha l'ambizione di proporre una soluzione definitiva ai linguaggi programmativi. Esso assomma le caratteristiche di FORTRAN, COBOL e ALGOL 60 e introduce alcune caratteristiche nuove, anche se assai poco assestate da un punto di vista teorico, quali la concorrenza e la gestione di eventi anomali. Il risultato è un linguaggio complesso, difficile da capire e usare, con grossi problemi di efficienza; di conseguenza, il linguaggio ha conosciuto una certa diffusione finché è stato sostenuto dai costruttori di elaboratori, ma va ora via via scomparendo.
Il BASIC per molti aspetti rappresenta un vero passo indietro rispetto al livello dei linguaggi di programmazione: offre costrutti linguistici piuttosto rozzi e non si preoccupa di supportare uno stile di programmazione adeguato. Ciò malgrado, il linguaggio ha introdotto una innovazione di grande importanza, offrendo per la prima volta un ambiente di sviluppo interattivo, nel quale il programmatore colloquia con la macchina nella progressiva messa a punto del programma. Questo stile di lavoro è diventato sempre più diffuso, anche se fortunatamente gli strumenti linguistici messi a disposizione del programmatore sono di livello ben più alto rispetto al BASIC originario, il quale, a sua volta, si è andato evolvendo fino a incorporare le migliori caratteristiche degli altri linguaggi.
Nella seconda metà degli anni Sessanta si avvertì l'esigenza di fornire non solo un efficiente linguaggio di programmazione, ma un linguaggio che favorisse o addirittura imponesse un buono stile di programmazione e rendesse agevole analizzare i programmi per dimostrarne la correttezza. È proprio a questo scopo che nacque il Pascal, al fine di educare alla programmazione disciplinata, scomponendo per raffinamenti successivi il problema da risolvere in sottoproblemi via via più semplici. Data la sua semplicità, e al tempo stesso la sua potenza, il linguaggio si diffuse sempre più, inizialmente come linguaggio didattico nelle università e successivamente anche nel mondo applicativo.
L'ALGOL 68 nacque come evoluzione dell'ALGOL 60; come il suo illustre predecessore, il linguaggio, dopo un periodo di diffusione nelle università europee, è oggi caduto in disuso. Esso ha però un'importanza storica, in quanto si tratta del primo linguaggio del quale sia stata fornita una definizione formale completa, sia per gli aspetti sintattici che per quelli semantici.
Il Simula 67 è il capostipite dei cosiddetti linguaggi orientati agli oggetti, che hanno recentemente acquisito un'importanza crescente e nei quali vengono riposte molte speranze. Pur nella varietà delle proposte linguistiche oggi offerte, un linguaggio orientato agli oggetti può essere caratterizzato come linguaggio che offre come meccanismo di modularizzazione un costrutto (normalmente chiamato classe) con il quale si possono definire tipi di dati astratti. Il linguaggio permette inoltre di organizzare le classi in una gerarchia attraverso la relazione di ereditarietà. Una classe erede di un'altra ne definisce un'estensione; il meccanismo di ereditarietà consente pertanto di modificare una classe esistente non operando direttamente su di essa, ma progettandone le modifiche in una classe erede. Queste caratteristiche linguistiche, che favoriscono una struttura del programma altamente modulare, sono presenti per la prima volta nel Simula 67.
Il linguaggio C nasce come linguaggio in cui viene scritto il sistema operativo UNIX. Si tratta forse del primo esempio di uso di un linguaggio di alto livello per produrre software di sistema, software cioè che si pone come intermediario tra l'hardware e le applicazioni, e che quindi richiede elevata efficienza. Il successo di UNIX, che è ormai diventato un sistema standard, la disponibilità di ottimi compilatori e la facilità con la quale C e UNIX interagiscono hanno costituito una spinta poderosa per la diffusione del linguaggio.
Il PROLOG è il primo esempio di linguaggio per la programmazione logica. Anche se esso impone severe limitazioni rispetto alle formule logiche che possono essere scritte, per la prima volta mostra come formule dichiarative possano essere risolte da un interprete automatico. Il linguaggio, malgrado la novità dell'approccio proposto, restò per molti anni ignoto al mondo applicativo e venne riscoperto negli anni Ottanta, da un lato, in seguito all'attenzione a esso rivolta dal cosiddetto progetto nazionale giapponese di elaboratori della v generazione e, dall'altro, dal diffondersi delle applicazioni dei sistemi esperti, di cui abbiamo fatto già cenno in precedenza.
Una versione per certi aspetti semplificata rispetto alla programmazione logica in PROLOG è quella offerta dai cosiddetti sistemi a regole, nei quali la conoscenza di un certo specifico dominio è fornita mediante una speciale sintassi di regole, e un interprete del linguaggio (il cosiddetto motore inferenziale) risolve semplici problemi a partire da tali regole. Sistemi di questo tipo hanno iniziato ad avere una certa diffusione anche industriale negli anni Ottanta.
Dopo un periodo di scarsi risultati industriali, ma d'intensa attività di ricerca condotta nella seconda metà degli anni Settanta (i cui risultati più interessanti sono i linguaggi Concurrent Pascal e CLU), nel 1980 nacque un importante nuovo linguaggio convenzionale: Ada. Ada nacque su commissione del dipartimento della Difesa degli USA e si presenta come linguaggio di tipo universale, ma in particolare orientato alle applicazioni di sistema in cui l'elaboratore deve interagire e controllare un ambiente esterno; si tratta pertanto di applicazioni che vengono comunemente chiamate concorrenti e in tempo reale. Ada si presenta come un netto passo in avanti rispetto al Pascal, dal quale prende le mosse. L'evoluzione si manifesta in tre direzioni fondamentali: l'offerta di costrutti di modularizzazione più ricchi, la possibilità di definire più attività concorrenti e la possibilità di programmare le risposte da dare agli eventi anomali. Accanto ad Ada, nacque e si diffuse il linguaggio Modula-2 (definito da N. Wirth, il creatore del Pascal). È per certi aspetti una versione semplificata e ridotta di Ada.
Gli anni recenti hanno portato a una diffusione crescente di nuovi linguaggi orientati a oggetti. Il linguaggio che per primo ha imposto al mondo applicativo l'attenzione verso questa categoria di linguaggi è Smalltalk-80. A risvegliare l'attenzione verso questo approccio contribuì non poco l'importanza che Smalltalk-80 assegna all'ambiente operativo di supporto, e in particolare alla cosiddetta interfaccia utente, basata su una gestione dello schermo dell'elaboratore personale che ''simula'' la scrivania di lavoro. Lo schermo appare come suddiviso in porzioni sovrapponibili (dette finestre) e l'interazione avviene in maniera molto flessibile attraverso menù, attivabili usando un dispositivo di puntamento a zone dello schermo (il mouse).
Tra i l. p. orientati a oggetti oggi più diffusi citiamo il C++, un'estensione del linguaggio C; tra i più interessanti, citiamo Eiffel. Il paradigma di programmazione orientato agli oggetti, comunque, non soltanto ha invaso il settore dei linguaggi convenzionali, ma ha provocato la nascita di estensioni ai linguaggi logici e funzionali, molte delle quali di tipo sperimentale. Fra quelle più note anche a livello applicativo, citiamo il CLOS, un'estensione del linguaggio LISP.
Bibl.: C. Ghezzi, M. Jazayeri, Programming language concepts, New York 19872 (trad. it., Concetti dei linguaggi di programmazione, Milano 1989); B. Meyer, Introduction to the theory of programming languages, Englewood Cliffs 1990; Software engineer's reference book, a cura di J. McDermid, Oxford 1991 (v. sezione Programming languages); O. D'Antona, E. Damiani, Ambienti esecutivi e di sviluppo dei linguaggi di programmazione, Milano 1992.