La Torre di BaBasic - 5
Torre di controllo
Concludiamo con questa puntata la prima parte della nostra visita nella giungla dei linguaggi: è il turno delle istruzioni di controllo, che regolano l'esecuzione del programma.Dopo avere esaminato il loro funzionamento, saremo in grado di passare, nei prossimi numeri, ad esaminare alcuni aspetti esoterici dei linguaggi più evoluti: le eccezioni, i cluster, la programmazione concorrente.
Come abbiamo osservato più volte, un programma è semplicemente una sequenza di semplici istruzioni che il calcolatore esegue pedissequamente, dalla prima all'ultima. Il computer, infatti, pur essendo una macchina estremamente complessa, è in grado di compiere soltanto operazioni semplicissime, come la somma di due numeri interi o il confronto di due numeri, sempre interi, per stabilire quale sia il maggiore.
Per di più, mentre nel passato prossimo la tendenza è sempre stata verso microprocessori (il componente del computer che esegue il programma) sempre più complessi e in grado di compiere operazioni sempre meno banali, oggigiorno la direzione si è invertita: gli ingegneri elettronici lavorano per la realizzazione di processori meno capaci, ma molto più veloci, lasciando così un maggior carico ai programmatori.
Nonostante questa evidente limitazione delle capacità di un computer, i programmi che vengono creati per queste macchine raggiungono vette notevolissime di complessità, e sono in grado di operare su intere classi di problemi. Un programma può giocare molto bene a scacchi, anche se le combinazioni possibili di pezzi sulla scacchiera sono tali e tante da impedire la loro trattazione esaustiva. Un programma può comportarsi in due modi diversi quando si trova di fronte la stessa situazione - questo è fondamentale in diversi videogiochi - anche se questo sembra contraddire la definizione di "sequenza di istruzioni che il computer esegue nell'ordine".
La contraddizione non esiste, dato che tra le semplici operazioni che il processore è in grado di compiere sono sempre presenti le operazioni condizionali e le operazioni di salto: è di esse che ci occupiamo in questo articolo.
L'operazione di salto, che viene normalmente chiamata Jump (dal verbo inglese che significa "saltare") o Go To (che significa "andare a") ha l'effetto di far deviare il processore dalla sua marcia a rullo compressore dentro un programma. Con l'istruzione Jump al processore viene fornita una nuova locazione di memoria dalla quale procedere con l'esecuzione:
DEMO EQU * ;Inizio di una routine in Assembler
JMP LAGGIU ;Il processore salta "laggiù"
STP ;Questa istruzione non viene mai eseguita!
LAGGIU EQU * ;Il processore continua da qui.
10 REM Esempio equivalente in Basic
20 GOTO 40
30 PRINT: REM Questa istruzione non è eseguita
40 END
Sino dalla prima puntata abbiamo visto una istruzione Assembler che è parente stretta della Jump, l'istruzione chiamata Jump To SubRoutine (il nome viene normalmente abbreviato in JSR) che viene utilizzata per saltare sino ad una procedura, eseguirla e poi riprendere dal punto in cui ci si era interrotti.
Per quanto riguarda le operazioni condizionali, si tratta semplicemente delle operazioni che poermettono di eseguire una data parte del programma solo se una certa condizione risulta vera. L'istruzione "if", presente in tutti i linguaggi di programmazione, è l'esempio canonico:
if Continbanca < 0 then
SiamoNeiGuai
else
TuttoBene;
Se andiamo a sbirciare al livello più basso, però, il livello del linguaggio macchina, ci rendiamo immediatamente conto che le istruzioni condizionali sono parenti strette delle istruzioni di salto. Infatti per realizzare al pezzo di programma Pascal appena visto corrisponde in Assembler la sequenza:
LDA Continbanca ;Leggi il valore di Continbanca
CMP #00 ; e paragonalo a zero
BLT SiamoNeiGuai ;Se è minore, vai a...
JMP TuttoBene ; altrimenti vai a...
Blt è la sigla di "branch if less than", che significa "salta se è inferiore a". Per inciso ci preme far notare che negli esempi di codice Assembler che corredano questi articoli abbiamo usato l'assembler tipico dei processori della famiglia 65xxx, che in molti reputano essere il più semplice da comprendere: tuttavia, dato che questo non è un corso di Assembler, gli esempi proposti sono quasi sempre scritti per far comprendere come funzionano certi costrutti, e sono poco efficienti, o per nulla efficienti, dal punti di vista della programmazione assembler.
Il nostro frammento in assembler mostra l'uso di Blt, uno dei tipici operatori di paragone e salto dell'assembler. Questi operatori sono solitamente chiamati operatori di branch. Il termine inglese branch indica il ramo di un albero, e sta a significare che questi operatori realizzano una diramazione del codice.
Se il paragone è soddisfatto, e cioé se il valore di Continbanca è effetticamente minore di zero, l'esecuzione del programma prosegue con l'istruzione chiamata SiamoNeiGuai, mentre nel caso opposto il processore passa ad eseguire il codice indicato con TuttoBene.
Le istruzioni di branch permettono il trattamento di un numero estremamente piccolo di casi, che si rivela tuttavia sufficiente per trattare ogni tipo di operazione condizionale. Ad esempio, qualunque linguaggio superiore permette di paragonare tra di loro due stringhe di caratteri:
if NomeAutore = 'Luca Accomazzi'
then Write ('Un buon articolo.');
Non tutti i processori permettono però il confronto diretto di due sequenze di caratteri: in questo caso è il creatore del linguaggio che fornisce una subroutine per trattare il caso.
Nella tabella 1 potete confrontare quali istruzioni di paragone siano tipicamente disponibili sui microprocessori. Abbiamo preso in esame le tre famiglie più diffuse oggigiorno: la famiglia 68 (Macintosh, Amiga, Atari St), la famiglia 65 (Apple II, Commodore 64) e la famiglia 80 (Pc Ibm e ammiratori). Le prime due famiglie sono notevolmente simili in questo rispetto, denunciando così la loro lontana parentela (discendono entrambi dal vecchio 6800).
Tabella 1
Uguaglianza con zero |
A questo punto possiamo introdurre le strutture di controllo complesse che troviamo nei linguaggi di programmazione evoluti - Algol e discendenti. La struttura di controllo più semplice è lo "if", che tutti i lettori certamente conosceranno, dato che è universalmente diffusa. L'istruzione prende la forma di "if condizione then azione"; il vocabolo inglese "if" significa "se", e "then" significa "allora". E' possibile aggiungere un ramo "else" - questo termine significa "altrimenti" - per trattare il caso in cui la condizione resta insoddisfatta.
if a = b then write ('Sono uguali')
else write ('Sono diversi');
Una osservazione molto importante: è dimostrato matematicamente che qualunque struttura di controllo complessa, una qualunque tra quelle che stiamo per esporre nel resto dell'articolo, è realizzabile utilizzando l'istruzione if e l'istruzione jump, ovvero usando le istruzioni di branch dell'assembler. Questo ci garantisce che è possibile costruire operazioni complesse a piacere usando come mattoni le semplici istruzioni dell'assembler, così come nella geometria euclidea si sviluppano complessi teoremi partendo da pochi semplici assiomi.
Il secondo costrutto di controllo che osserviamo è il ciclo "for". Viene utilizzato per ripetere l'esecuzione di alcune istruzioni per un numero prefissato di volte. Per stampare cinque volte il nome della nostra rivista non è necessario usare cinque volte la stessa istruzione:
WriteLn ('Bit');
WriteLn ('Bit');
WriteLn ('Bit');
WriteLn ('Bit');
WriteLn ('Bit');
Si scrive invece:
for i := 1 to 5 do
WriteLn ('Bit');
L'uso della variabile di controllo 'i' è necessario per usiun poco più complessi; per esempio, per stampare i numeriinteri dallo zero al cento si scrive:
for i := 1 to 100 do
WriteLn (i);
Queste righe sono equivalenti a:
i := 1;
LOC: WriteLn (i);
i := i + 1;
if i < 100 goto LOC
In Pascal il "for" ha solo due possibilità: marciare inavanti o all'indietro. La marcia avanti è quella che abbiamomostrato nelle scorse righe, mentre quella all'indietro prende laforma di:
for i := 100 downto 1 do
WriteLn (i);
In questo modo si stampano in ordine decrescente i numeri tra il100 e lo 0. Pascal fornisce un costrutto for estremamente debolequando lo paragoniamo a tutti gli altri linguaggi evoluti. In Basic,per esempio, ci è concesso di scrivere:
10 FOR i = 0 TO 100 STEP 5
20 PRINT i
30 NEXT i
L'effetto di questo programma Basic è di stampare i numeri 0, 5, 10, 15, 20 e così via sino al 100. Il termine "step" significa "a passi di", mentre il termine "next" significa "il prossimo", ed indica la fine di uno dei passi del ciclo for.
Il costrutto for di gran lunga più potente viene messo a disposizione dal linguaggio Algol 68, dove possiamo scrivere ad esempio:
for i from 1004 by 4 to 1996 while (i mod 100) <> 0 do
Write (i)
od
Questa semplice (!) riga di programma scrive sullo schermo quali sono stati gli anni bisestili nel nostro millennio. Per capire come funziona dobbiamo prima ricordare che l'operatore Mod restituisce il resto della divisione tra due numeri. Per esempio, 17 Mod 5 vale 2 perchè 17 / 5 = 3 col resto di 2.
Gli anni bisestili sono quelli il cui numero è divisibile esattamente per 4 con l'eccezione dei multipli di 100. Per esempio, il 1988 è un anno bisestile, perchè 1988 diviso 4 è esattamente uguale a 497. Il 1989 non sarà bisestile, perchè 1989 / 4 = 497,25. Il 1900 non è stato bisestile anche se 1900 / 4 = 475, perchè 1900 è anche multiplo di 100.
Viene eseguito un ciclo for sulla variabile i, con valore tra 1004 e 1996 che viene incrementato di 4 ad ogni ripetizione del ciclo. Si stampa il valore di i purchè i non sia un multiplo di 100 (la condizione while). Quella singola riga di Algol è equivalente al seguente programma scritto usando if e goto:
i := 1004;
LOC: if (i mod 100) = 0 then goto SALTA;
WriteLn (i);
SALTA: i := i + 4;
if i <= 1996 then goto LOC;
C'è un costrutto in grado di rendere ancora più espressivo il nostro for, e lo troviamo implementato in CLU, un linguaggio piuttosto recente che ha molti punti di contatto con Ada.
Immaginate di aver creato una struttura dati ad albero, come quella che vedete in figura. Nell'albero sono stati memorizzati i dati dei vostri antenati, dai genitori fino ai nonni e oltre, e voi avete creato un programma che vi permette di manipolare l'albero per qualche motivo.
NodoAlbero = record
Nome: String;
Cognome: String;
Padre: ^NodoAlbero;
Madre: ^NodoAlbero
end NodoAlbero;
Esistono tre differenti metodi (il nome corretto è algoritmi) complessi che permettono di visitare tutti i dati di un albero: si chiamano preorder, postorder e inorder, e si distinguono a seconda dell'ordine in cui recuperano i dati.
Ma come è possibile utilizzare uno degli algoritmi nel nostro programma? Con il potente costrutto for di CLU ci basta scrivere una procedura che esegue l'algoritmo scelto e restituisce ogni volta un nodo differente sinchè non ha percorso tutto l'albero.
Inorder = iter (a: albero) yelds (NodoAlbero)
{esegue l'algoritmo}
yield (QuestoNodo)
end Inorder
Possiamo poi utilizzare la procedura Inorder in un normale ciclofor.
for UnNodo : NodoAlbero in Inorder (AlberoGenealogico) do
if (UnNodo.Cognome = CognomeCercato) and (UnNodo.Nome =NomeCercato) then
{Comunica che il nodo è statotrovato}
Gli altri due costrutti di controllo che troviamo comunemente implementati nei linguaggi di programmazione di alto livello sono il case e il while. Possiamo dimostrarli entrambi con il seguente pezzo di programma scritto in Pascal:
repeat
WriteLn ('Quanti anni hai?');
Read (Anni);
case Anni of
1..10: WriteLn ('Sei un bambino!);
11..20: WriteLn ('Sei giovane!');
{ eccetera eccetera }
80..99: WriteLn ('Sei vecchio!')
end;
until Anni = 0;
E' piuttosto semplice capire il funzionamento del programmasapendo che repeat..until in inglese significa ripeti..finchè."Case Anni of" significa invece "nel caso in cui Anni valga". Usandoif e goto possiamo esprimere case con le istruzioni:
if Anni <=10 then goto L1;
WriteLn ('Sei un bambino!');
L1: if Anni <=20 then goto L2;
WriteLn ('Sei giovane!');
L2: if Anni <= 30 then goto L3;
{ eccetera }
Il repeat..until e il suo fratello gemello while..do sono semplicemente della variazioni del ciclo for da utilizzare quando non si sa in anticipo quante volte il ciclo va eseguito. Per esempio, se volete scrivere un programma in grado di leggere dei dati dal disco per poi inviarli alla stampante non potete usare un semplice for, dato che non sapete di quante righe sia composto il documento su disco. Si utilizza invece il costrutto repeat, che fa ripetere il ciclo sinchè resta soddisfatta la condizione espressa.
L1: Read (DaDisco, UnaFrase);
Write (SuStampante, UnaFrase)
if not ErroreDiLettura then goto L1;
Nel nostro esempio è evidente un grosso problema: cosa significa "errore di lettura"? Come lo si riconosce, e come viene trattato? Il trattamento delle eccezioni sarà l'argomento della prossima puntata di questa serie.