La Torre di BaBasic - 4
La Torre di BaBasic - 4
In una serie dedicata ai linguaggi, una puntata dedicata ad uno solo di questi può sembrare anomala: ma se il linguaggio in questione è Ada, possiamo aspettarci che disponga di tutto quello di cui vogliamo parlare, e anche qualcosa di più.Nella scorsa puntata, come forse ricorderete, abbiamo esaminato i tipi di dati, assumendo il punto di vista di Pascal.
I dati sono memorizzati in variabili, e queste sono caratterizzate dal tipo: per esempio una variabile chiamata CostoDollaro e contenente il valore 1700 è una variabile di tipo Integer (numero intero). Il tipo intero è un tipo semplice, così come lo sono il numero reale, cioé un numero con cifre decimali, o il carattere alfanumerico.
Var
Età: integer; { tipi semplici }
Lettera: char;
Pigreco: real;
begin
Età := 23;
Lettera := 'A';
Pigreco := 3.14
end;
Una variabile di tipo complesso memorizza più di un dato: ad esempio un vettore (o array) è una variabile che memorizza tanti valori omogenei, come vediamo nella tabella 1.
Tabella 1. Un esempio di vettore, in Pascal.
Type
Valuta = (Dollaro, Franco, Marco, Sterlina);
Var
Cambio: array [Dollaro..Sterlina] of integer;
Begin
Cambio [Dollaro] := 1206;
Cambio [Franco] := 217;
Cambio [Marco] := 736;
Cambio [Sterlina] := 2191
End.
Un record è una variabile che memorizza valori disomogenei: per esempio, un record può venire usato per memorizzare Nome, Cognome, età e numero di telefono di una persona.
Type
Primato = record
NomeAtleta: String;
CognomeAtleta: String;
Nazionalità: String[3]
end;
Var
Asta: Primato;
begin
Asta.NomeAtleta := 'Sergei';
Asta.CognomeAtleta := 'Bubka';
Asta.Nazionalità := 'URS'
end;
In questa puntata espanderemo questi concetti, assumendo quasi sempre il linguaggio Ada. Potremo così terminare la trattazione delle variabili e passare ad argomenti più complessi e ancora più interessanti.
Riprendiamo in mano il nostro vettore. Sinora l'abbiamo visto dichiarare - ricordate che i vettori e i record vanno dichiarati in un programma? - più o meno così:
NumeriDiTelefono: Array [1..200] of Long Integer;
Qual è la limitazione di questo modo di usare gli array? Un programmatore Basic può immediatamente identificare una limitazione che in Basic, Apl e molti altri linguaggi non esiste.
Se stiamo creando un programma che deve venire usato come agendina telefonica possiamo pensare di usare un vettore come quello dichiarato con
NumeriDiTelefono: Array [1..200] of Long Integer;
ma dovremo limitare l'utente del programma a non più di 200 annotazioni.
Duecento annotrazioni potrebbero essere poche per un manager particolarmente impegnato, e questo renderebbe inservibile per lui il nostro programma. E pensate che disastro sarebbe se il manager scoprisse questa limitazione solo dopo aver passato due o tre serate a battere nel programma i primi 200 numeri di telefono della sua agenda cartacea!
D'altra parte, per una persona riservata 200 numeri potrebbero essere troppi: il nostro programma, sovradimensionando il vettore, sprecherebbe parecchia memoria. Lo spreco di memoria, anche se al giorno d'oggi i computer nascono con interi megabyte in dotazione, è comunque una cattiva abitudine da evitare.
In Basic potremmo però scrivere un programma fatto così
Print "Quanti numeri telefonici dovrò memorizzare?"
Input NDT
Dim NumeriDiTelefono (NDT)
Questo avrebbe un effetto simile alla dichiarazionePascal:
NumeriDiTelefono: Array [0..NDT] of Long Integer;
{ che in Pascal non sarebbe accettata }
Se il nostro linguaggio ci dà la possibilità di definire le dimensioni del vettore durante l'esecuzione del programma - se possiamo cioé usare una variabile per dimensionare il vettore - diremo che il linguaggio fornisce un vettore dinamico.
Un linguaggio, come Basic o come Apl, in cui il programma viene eseguito così come lo avete scritto, senza bisogno di usare un compilatore perchè il computer possa eseguirlo, èchiamato linguaggio interpretato. Per un linguaggio interpretato è abbastanza semplice permettere l'uso di vettori dinamici, mentre per un linguaggio compilato questo è particolarmente problematico. Questo dipende dal diverso modo in cui le due classi di linguaggi trattano le variabili - entrare nei dettagli tecnici richiederebbe più spazio di quanto ne abbiamo a disposizione ora, ma torneremo presto sull'argomento.
Pascal non è interpretato, e non permette l'uso dei vettori dinamici. Ada, pur essendo un linguaggio compilato, ne consente comunque l'uso:
Type
VettoreStatico is Array (integer range 1..100) ofinteger;
VettoreDinamico is Array (integer range < >) ofinteger;
Notate che nel nostro esempio Basic abbiamo supposto di chiedereall'utente quale sia il numero massimo di nomi da memorizzare:
Print "Qual è il massimo numero di numeri damemorizzare?"
Input NDT
Probabilmente, però, un programma ben fatto provvederebbe da solo a dimensionare il vettore dei dati, dopo aver scoperto quanta memoria sia a disposizione nel computer in uso. Il vettore dinamico si chiama così proprio perchè viene dimensionato mentre il programma sta girando, mentre il vettore statico viene dimensionato dal programmatore a programma fermo.
Chiudiamo la trattazione dei vettori dinamico mostrando come sia possibile con Ada scrivere una funzione che restituisce la somma di tutte le componenti di un vettore dinamico, qualunque sia il numero di celle che esso contiene. Notate che in Ada non è necessario dichiarare le variabili usate nei cicli di tipo for, come la variabile i dell'esempio, in quanto questo è fatto automaticamente dal linguaggio.:
Function Somma (A: VettoreDinamico) return integer is
var
somma: integer := 0;
begin
for i in A'first..A'last loop
somma := somma + A(i);
end loop;
return somma;
end;
Ma andiamo oltre. Immaginiamo di aver costruito il programma "agendina" lasciando all'utente la possibilità di dichiarare all'inizio quale debba essere il massimo numero di indirizzi memorizzabili.
L'utente, però, può sbagliare, per malizia o in buona fede. Come ogni buon programmatore deve sempre ricordare...
E' impossibile fare un programma a prova di stupido, perchè gli stupidi sono troppo ingegnosi |
La nostra agendina sarebbe inizialmente realizzata con un vettore vuoto, senza nessuna componente; in seguito, ogni qual volta fosse necessario aggiungere un indirizzo, noi amplieremmo di una unità le dimensioni del vettore, usando la nuova cella per memorizzare l'ennesimo indirizzo. Il vettore che ha questa capacità di adattarsi alle esigenze mentre lo usiamo viene chiamato vettore flessibile. Non confondetelo con il vettore dinamico, le cui dimensioni vengono scelte durante l'esecuzione del programma ma che poi non possono più venire cambiate.
Il vettore flessibile non è disponibile in nessuno dei linguaggi che abbiamo già citato su queste pagine: non Ada, non Basic, non Pascal né C né Lisp né Apl. Però...
Una stringa, cioé quel tipo di dato usato per memorizzare parole e frasi, è in effetti un vettore flessibile di caratteri. Quando voi create una variabile stringa, create un vettore flessibile vuoto. Inventandoci un po' di sintassi dal suono pascaleggiante, possiamo scrivere che:
Var
NomeCanzone: String; { equivale a }
NomeCanzone: flexible array [1..?] of char;
Quando assegnate un valore alle stringa, cambiate la lunghezza del vettore e contemporaneamente assegnate dei nuovi valori ai componenti, che sono variabili di tipo carattere.
NomeCanzone := 'Scotland the Brave';
Che equivale a:
NomeCanzone [1] := 'S';
NomeCanzone [2] := 'c';
NomeCanzone [3] := 'o';
NomeCanzone [4] := 't';
NomeCanzone [5] := 'l';
NomeCanzone [6] := 'a'; { eccetera... }
Pertanto scrivere:
NomeCanzone := 'Scotland the Brave';
equivale a ridimensionare il vettore a 18 elementi, assegnandoli nel contempo.
Allora tutti i linguaggi che hanno il tipo stringa hanno anche gli array flessibili? No. Hanno solo un tipo particolare e specializzato di array flessibile. Per di più, quasi tutti i linguaggi citati limitano le stringhe ad una lunghezza massima di 255 caratteri (fa eccezione il C), mentre un vero array flessibile non dovrebbe avere limitazioni di questo tipo.
Il più conosciuto tra i linguaggi a disporre davvero di array flessibili è lo Algol 68.
In Algol 68 esistono i vettori flessibili - per la verità nel gergo di Algol vanno chiamati multipli flessibili - ma sono davvero scomodi da usare. Con:
flex [1:0] int tiramolla;
noi dichiariamo di voler creare un vettore flessibile chiamato tiramolla, che conterrà dei numeri interi ed inizialmente vuoto. Se poi vogliamo farlo diventare un vettore di due componenti alle quali assegnamo i valori 7 e 5000 scriviamo:
tiramolla := (7, 5000);
Il problema con Algol sta nel fatto che, per modificare le dimensioni del vettore flessibile dobbiamo assegnare tutti i valori delle variabili semplici che lo compongono, esattamente come succede in tutti gli altri linguaggi quando usiamo le stringhe. Nel nostro esempio dell'agendina telefonica, però, questo sarebbe inaccettabile.
Gli array flessibili, dunque, sono il massimo cui potremmo ambire, ma sono talmente difficili da realizzare per chi crea il compilatore di un linguaggio che non si trovano mai disponibili. Per la verità dobbiamo aggiungere che, anche quando ci sono, sono di una lentezza esasperante, tanto da sconsigliarne l'uso.
Con questo commento chiudiamo la nostra serie di osservazioni sui vettori. Notate che non abbiamo risolto il problema dal quale eravamo partiti, l'esempio dell'agenda; questo non significa che non sia possa realizzare un'agenda usando un calcolatore, ma solo che il vettore non è lo strumento (la struttura dati) adeguato a risolvere il problema. In un caso del genere vengono comode, invece, le variabili dinamiche di cui abbiamo accennato nella seconda puntata.
Voltiamo pagina e passiamo all'overloading, un'altra delle mille capacità dell'insuperabile Ada.
Perdonate la banalità se lo facciamo notare, ma non potremmo fare nulla coi dati se non avessimo la possibilità di compierci operazioni, giusto? Se pensiamo al tipo semplice che abbiamo chiamato Integer, sappiamo che esistono gli operatori aritmetici, come + e -, per elaborarli. Se pensiamo al tipo Real, il numero decimale, troviamo ancora una volta gli operatori + e -, e se passiamo alle stringhe non è improbabile che sia possibile per noi usare, ancora una volta, l'operatore + su di esse:
10 + 10 = 20
4.3 + 7.1 = 13.4
"Ciclope" + " " + "Strabico" = "Ciclope Strabico"
Se ci pensate un momento, dunque, noi possiamo usare lo stesso simbolo per indicare quelle che sono in realtà tre operazioni ben distinte. (Se non vi sembra che sommare due numeri interi sia cosa ben differente dal sommare due numeri decimali, questo significa che - beati voi - non avere mai dovuto affrontare i meandri della matematica avanzata. Tuttavia, potete rendervi conto del fatto che il computer rappresenta in modo ben diverso tra di loro le due classi di numeri, come testimonia il fatto che l'occupazione di memoria è differente nei due casi. Ne consegue che il sottoprogramma che viene usato per eseguire la somma è ben diverso nei due casi. Chiusa parentesi).
Si dice che l'operatore + è un operatore overloaded, ovvero sovraccarico, di significati: possiamo usarlo per compiere operazioni distinte. Questa caratteristica è particolarmente utile, dato che in fin dei conti la logica che sta dietro alle operazioni in questione è estremamente simile, e ci risulta semplice pensare a una generica operazione di somma che funziona altrettanto bene e prevedibilmente sui diversi tipi. La frase seguente, per esempio, non ha bisogno di spiegazioni, ed è intuitivamente comprensibile:
Coppia = Moglie + Marito.
Vi sembra banale? Pensate allora che in Pascal esistono due differenti operatori, / e DIV, per la divisione tra numeri reali e tra numeri interi...
L'overloading è tanto naturale che i creatori di Ada hanno pensato di renderlo disponibile a tutti i programmatori.
Ammettiamo di star scrivendo un programa di grafica, avendo definito
Type
Colore is (Bianco, Rosso, Verde, Blu, Nero); -- eccetera...
Ci interessa permettere la miscelazione dei colori, permettendo ad esempio di creare il rosa aggiungendo del bianco al rosso. Ci basta scrivere una funzione fatta come segue:
Function "+" (A, B: Colore) return Colore;
In questo modo abbiamo overloaded ulteriormente l'operatore di somma: siamo autorizzati ad utilizzarlo per sommare numeri interi, numeri reali e colori nel nostro programma. Se nel programma si incontra
x := Rosso + Verde;
questo equivale a richiamare la nostra funzione chiamata "+" con i parametri 'Rosso' e 'Verde'. Normalmente lo overloading viene utilizzato con i package, una caratteristica di Ada che abbiamo citato nella prima puntata e sulla quale torneremo in seguito, e i generic, un costrutto ancora superiore.
Tra i trucchi di varia utilità che Ada consente con le variabili ricordiamo anche...
* La capacità di dettagliare dove va memorizzata una variabile in memoria:
For Variabile Use At 16327;
* La capacità di specificare quanta memoria va riservata ad una variabile:
Type Puntatore is Access Integer;
For Puntatore'Storage_Size Use 24;
* La conversione su richiesta di una variabile tra più formati di memorizzazione:
Type A is Now B
For B use -- eccetera
* La capacità di inserire direttamente procedure in Assembler nel codice Ada:
Pragma Inline (AssemblyProc);
* ...e svariate altre mirabilie che non citiamo per non ridurre questo articolo ad una sviolinata su quel linguaggio.
Tutto questo ha un senso logico che va al di là del desiderio di fornire al programmatore uno strumento potente e flessibile: Ada è stata creata, come altri linguaggi prima di lei, per garantire la sicurezza nei grandi programmi. Essendo stata creata, su ordinazione del ministero della difesa americano, per risolvere i problemi che si verificavano con allarmante frequenza, Ada fa tutto il possibile per facilitare il lavoro di più persone su uno stesso programma, nel contempo eseguendo tutti i controlli possibili per evitare che l'errore di un solo programmatore guasti gli sforzi di tutti.
Quando un gruppo di duecento persone deve lavorare su un progetto da un milione di righe di codice o giù di lì, si può star certi che la maggior parte dello sforzo profuso nello sviluppo andrà nello sforzo organizzativo: diventa fondamentale disporre di uno strumento come Ada.
Prendete il linguaggio C, e dite a due programmatori di sviluppare un programma, assegnando a ciascuno di loro lo sviluppo di metà del codice.
Il compilatore C permette lo sviluppo di un programma in pezzi, come il compilatore Ada, ma al contrario di Ada non effettua controlli sulla coerenza quando i pezzi vengono messi insieme.
Il primo programmatore scriverà, per esempio, una funzione chiamata "PerProva", alla quale va passato un carattere, e il cui funzionamento non ci interessa:
void PerProva (QueSaraSara)
char QueSaraSara;
{
/* Corpo della funzione */
}
Il secondo programmatore deve utilizzare la funzione PerProva nel suo codice, ma per un malinteso (chissà cosa avrà mai capito) passa come parametro un numero reale:
Extern PerProva;
PerProva (6.5487);
Il compilatore C accetta questa assegnazione senza lamentarsi, perchè suppone che i due programmatori sappiano perfettamente quello che stanno facendo (il C, infatti, incoraggia l'uso di trucchi anche sporchi simili a questo per consentire la stesura di codice veloce ed efficiente). Se provate a fare qualcosa di simile ad Ada, però, sentirete i suoi lamenti a chilometri di distanza.
Si usa dire che Ada, come e più di Pascal, è un linguaggio strong typed, ovvero che constringe a fare severe distinzioni tra i tipi di variabili. Questo non significa che sia impossibile sommare una variabile intera con una reale, ma che è necessario dichiarare esplicitamente che lo si vuole fare, essendo consci del fatto, usando un apposito operatore per la conversione del tipo.
Il linguaggio C, come il Basic, lo Apl e la maggior parte dei linguaggi più vecchi, non presta eccessiva attenzione a questi dettagli, e non è dunque considerato un linguaggio strong typed.
Normalmente un linguaggio che permette, come fa Ada, la compilazione in pezzi separati non può essere strong typed. Questo accade perchè il compilatore accetta la compilazione separata dei due pezzi di codice - non può fare altrimenti, visto che sono entrambi corretti; il guaio è che non sono coerenti. Ada invece effettua tutta una serie di controlli, e impedisce l'incoerenza. La necessità di controllare la coerenza, però, rende alcuni meccanismi (quelli del package) criticabili - torneremo forse in futuro su questo argomento, che comunque è squisitamente tecnico e pertanto un poco noioso.
Ada realizza, come abbiamo detto, la difficile unione tra un linguaggio strong typed e un linguaggio che permette le compilazioni separate. Non solo: permette addirittura la compilazione separata di brani di codice strettamente dipendenti.
Immaginate di aver scritto una libreria per disegnare sullo schermo grafico delle semplici figure geometriche come cerchi, quadrati, rombi e gattini: voi desiderate però che sia un secondo programmatore, un esperto della natura fisica della macchina, a realizzare il codice di bassissimo livello che disegna i singoli punti.
In altre parole voi avete creato una procedura in grado di disegnare cerchi, quadrati, rombi e arcobaleni basandovi sulla capacità di disegnare un singolo punto sullo schermo, ma non avete scritto il codice che disegna il singolo punto. Questo è accettabile grazie al meccanismo di compilazione separata di cui abbiamo discusso sinora: ci sarà qualcuno che, separatamente, scriverà la procedura che disegna un singolo punto.
Aggiungiamo però una complicazione: è desiderabile che nessun altro pezzo di codice al di fuori della vostra procedura evoluta - quella in grado di disegnare cerchi, quadrati, rombi e bambine bionde - possa usare la semplice procedura che disegna il singolo punto. Potete immaginare che ci sia una ragione logica che impone limitazioni del genere: state sviluppando un programma per la Fiat e questioni di segreto industriale impongono di limitare l'accesso ai dati. State sviluppando una base di dati e desiderate che i programmatori che creeranno applicazioni che accedono alla base di dati non possano compiere accessi di basso livello che potrebbero distruggere l'integrità dei dati. State creando una libreria che verrà venduta ad altri programmatori, e non volete che essi, studiando il comportamento delle vostre routine, scoprano l'algoritmo che avete elaborato in cinque anni di fatica. Forse volete semplicemente mantenere un po' di pulizia nel vostro codice, nascondendo le procedure più semplici al codice evoluto del programma base.
Se rinunciassimo alla compilazione separata potremmo rispettare la limitazione che ci siamo imposti scrivendo:
Type
TipoFigura = (Cerchi, Quadrati, Rombi, Farfalle);
Coordinate = integer;
Procedure Disegnatore (Cosa: TipoFigura);
Procedure DisegnaPunto (X, Y: coordinate);
begin
{ Codice di DisegnaPunto }
end;
begin
{ codice di Disegnatore }
end;
Se però è necessario che Disegnatore e DisegnaPunto siano realizzate da due programmatori distinti siamo nei guai.
Con Ada siamo in grado di salvare sia la capra (la compilazione separata) che i cavoli (le procedure innestate), a differenza di quanto avviene in ogni altro linguaggio che abbiamo citato. Buttate un occhio sul codice che segue: c'è tutto quelo che serve, e può venire compilato. Manca la procedura che disegna i punti che può venire usata dentro Disegnatore e che può venire scritto separatamente: al suo posto si trova solo la dichiarazione, chiamata in gergo lo "stub" della procedura.
Package PerDisegnare is
Type
TipoFigura is Cerchi, Quadrati, Rombi,AuroreBoreali;
Coordinate is New Integer;
Procedure DisegnaPunto (in x, y: coordinate);
Procedure Disegnatore (Cosa: in TipoFigura);
Package Body PerDisegnare is
Procedure DisegnaPunto (in x, y:coordinate)
is separate; -- ecco lo stub
Procedure Disegnatore (Cosa: in TipoFigura) is
begin
-- codice di Disegnatore
end Disegnatore;
end PerDisegnare;
E con questo abbiamo finito anche questa puntata. Nella prossima puntata ci occuperemo delle strutture di controllo.