Articolo pubblicato sul n. 56 di MCmicrocomputer (Edizioni Technimedia Srl - Roma) nell'ottobre 1986

MCmicrocomputer


Linguaggi, istruzioni, parametri

di Andrea de Prisco

Dopo essere violentemente precipitati nei piu' infimi bassifondi di un calcolatore (sino al livello di microprogrammazione) questo mese risaliremo verso gli alti livelli dei moderni linguaggi di programmazione. Tratteremo circa i meccanismi di programmazione offerti da tali linguaggi, tra cui la strutturazioneCopertina del numero di MCmicrocomputer contenente l'articolo a blocchi, le dichiarazioni, le procedure e le funzioni.

Cenni storici

Come e' noto, un tempo esistevano solo le macchine nude e crude, e queste potevano essere istruite soltanto a colpi di "volgare" linguaggio macchine.

Il primo sforzo per aiutare il povero programmatore a non innervosirsi troppo a furia di numeri esadecimali, fu di inventare l'assember e il macroassembler, col quale era possibile parlare al calcolatore con un linguaggio appena un po' piu' civile: era perlomeno fatto di parole mnemoniche, con la possibilita' di definirsi anche nuove istruzioni a partire da quelle giA' esistenti (le Macro).

Subito dopo pero' si sent l'esigenza di mezzi piu' potenti per quel che riguarda le applicazioni scientifiche e in particolare i calcoli piu' complessi delle somme e moltiplicazioni disponibili a livello di CPU.

Servivano nuovi strumenti per trattare facilmente le equazioni, i sistemi e le funzioni matematiche in generale: una sorta di programmone che traduceva un linguaggio da un liv1llo piu' alto ad uno piu' basso: nel caso del fortran, da fortran a codice eseguibile dalla CPU.

Quasi parallelamente agli ingegneri che protendevano verso le soluzioni meccanizzate dei loro problemi matematici, gli "archivisti" parteggiavano per un linguaggio di programmazione piu' consono alla archiviazione e l'elaborazione automatica  dei dati. Per loro nacque il Cobol: COmmon Businers Oriented Language. Siamo ovviamente ancora agli albori dell'informatica: nonostante gli sforzi compiuti, programmare sia in fortran che in cobol qualcosa di non specificatamente previsto dai due linguaggi risultava tanto difficile quanto poteva esserlo per i calcoli il linguaggio macchina. E da quel momento un po' tutti nel mondo si sbizzarrirono a fare linguaggi di programmazione.

Nacquero linguaggi per trattare agevolmente le stringhe alfanumeriche (Snobol), liste o in generale oggetti non troppo numerici (LISP), processi e comportamenti di oggetti reali (SIMULA) e altro.

L'occhio con cui si guardava la nascita di un nuovo linguaggio di programmazione era comunque la massima comprensibilitA' anche da chi non avesse scritto il programma. Niente meccanismi per fare trucchetti strani, ma solo strumenti tutti puliti per una programmazione semplice e ordinata. Spari' il concetto di sottoprogramma per fare posto alle procedure e alle funzioni: niente gosub e return ma invocazione tramite il nome della stessa e nient'altro. Dei goto manco a parlarne: sono brutti semanticamente e soprattutto un programma zeppo di goto puo' far disperare chi cerca il bug nel proprio elaborato.

Grazie a nuovi costrutti di programmazione come l'IF-THEN-ELSE e il WHILE-DO e' stato dimostrato che se ne puo' fare comodamente a meno.

Algol-like

Il primo linguaggio di programmazione che sfrutto'  nuovi costrutti anti-goto e' stato l'algol 60. Si badi bene che per i fissati, il goto era pur sempre disponibile: semplicemente ne era altamente sconsigliato l'uso, anche perche' poteva creare non pochi  problemi il saltare da un punto all'altro dato che la programmazione algol e' strutturata.

Figura 1A partire da questo, nacquero in seguito altri linguaggi che si ispiravano all'algol 60: tutta la sua dinastia, comprendente algol 68, algol w, pascal, ada e altri, e' stata cosi' chiamata algol-like (tipo-algol).

Algol sta per ALGOLritmic Language, e il suo nome sta a sottolineare il fatto che il modo di programmare corrisponde praticamente ad una definizione precisa dell'algoritmo che stiamo implementando sul computer.

Un programma Algol-like e' strutturato a blocchi (vedremo meglio tra poco) ed ogni blocco e' diviso in due parti: la parte dichiarazioni e la parte comandi. Il programma stesso, per intero, e' un blocco: altri blocchi saranno in esso modificati (stile bambole russe) per descrivere l'algoritmo (o scrivere il progamma che e' la stessa cosa).

Nella zona dichiaraioni bisogna indicare tutte le variabili usate nel blocco e per oguna di queste il tipo (intero, reale, stringa array o altro) e eventualmente i parametri che occorrono, ad esempio il numero di elementi se e' un array. Le dichiarazioni, oltre a servire per allocare a tempo di compilazione o a tempo di esecuzione lo spazio necessario alle variabili, permettono durante la stesura di un programma di avere sempre sottomano tutta la lista delle variabili giA' usate in modo da non usare in modi diversi lo stesso oggetto. Sempreche' questo e' cio' che desideriamo: infatti la strutturazione a blocchi, unita alla possibilitA' di fare le dichiarazioni in ogni blocco, permette anche il nome di variabile denta due punti del programma, lo stesso nome di variabile denota due oggetti diversi.

E' arrivato il momento di fare qualche esempio. Dicevamo che un programma algol-like e' un blocco formato da due parti: dichiarazioni e comandi:

Begin

lista dichiarazioni

lista comandi

End

Le due parole chiave begin e end delimitano l'inizio e la fine del blocco. Prendiamo ora l'istruzione IF-THEN: la sua sintassi e':

IF (condizione) THEN (comando o blocco)

Ecco un punto dove possiamo usare un blocco piu' interno del blocco principale. Quasi tutti i comandi sono fatti in questo modo: se c'e' da far fare qualcosa a piu' di una istruzione, basta racchiderle tra begin e end in modo da creare un nuovo blocco (si noti che per i blocchi piu' interni, se non si usano nuove variabili o nuove occorrenze di variabili gia' esistenti, non sono necessarie le dichiarazioni). L'if di cui sopra, se la condizione e' verificata, non ha effetti (come in basic).

Ora vedremo la prima delle istruzioni anti goto. Il caso dovrebbe essere ovvio: a seconda di una condizione dobbiamo eseguire un insieme di comandi o un altro, come mostrto nel diagramma a blocchi di figure 1. In un linguaggio di programmazione vecchia maniera, cio' si realizza con almeno un salto, ad esempio in basic avremo:

10 IF A>0 THEN PRINT A: X=X+3: GOTO 30

20 B=A/2: A=A+1

30 ......

lo stesso programma in algol-like si scrive:

IF A>0 THEN BEGIN

              PRINT A

              X:=X+3

            END

       ELSE BEGIN

              B:=A/2

              A:=A+l

            END

appare evidente come nel secondo caso, una volta chiarita la convenzione che begin ed end delimitano un insieme di operazioni da compiere, il programmino algol-like non e' altro che la descrizione a parole (sebbene in inglese) del procedimento che volevamo descrivere (libera traduzione: se A e' maggiore di zero allora stampa A e ad X  associagli il valore di X + 3, altrimenti...ecc. ecc.).

Analogamente per il caso in cui dobbiamo eseguire un insieme di istruzioni finche' e' vera una condizione (figura 2). Continuiamo con gli esempi basic:

10 IF A<0 THEN 50

20 PRINT A

30 A=A-1

40 GOTO l0

50 .....

in algol-like scriveremmo un piu' pulito e piu' consono al vero significato:

WHILE A>0 DO BEGIN

              PRINT A

              A:=A-1

             END

Esiste pero' anche il caso contrario in cui la condizione e' posta dopo le istruzioni e si desidera ripeterle fino a quando una condizione non si verifica (fig 3). In basic:

10 PRINT A

20 A=A+l

30 IF A<0 THEN 10

 

in algol-like diventa:

REPEAT

  PRINT A

  A:=A+l

UNTIL A>0

piu' pieno di significato di cosi' si muore.

Infine, vorremmo mostrarvi come si implementa il caso in cui, a seconda del valore di una certa espressione bisogna eseguire un determinato pezzo di programma (fig 4). Ad esempio, in basic la situazione potrebbe essere:

20 IF A=3 THEN PRINT A: A=A-1: GOT050

30 IF A=5 THEN A=A-2: GOTO 50

40 IF A=2 THEN A=A+5

50 ......

 

scritto in un linguaggio algol-like diventa:

 

CASE A OF

 

0: PRINT "OK”

3: BEGIN

    PRINT A

    A:=A-1

   END

5: A:=A-2

2: A:=A+5

Si noti he per il caso 3, dovendo eseguire due comandi e' stato necessario racchiuderli in un blocco begin-end.

I blocchi

Finora abbiamo visto e usato i blocchi solo per racchiudere piu' istruzioni da eseguire al verificarsi di evento. Dicevamo, pero', che un blocco e' formato anche da una opzionale parte dichiarazioni tramite la quale possiamo definirci nuove variabili locali a quel blocco. Cio' significa essenzialmente due cose: primo al termine del blocco (una volta cioe' incontraro il corrispondente end) tutte le variabili li' dentro dichiarate vengono dealloccate, secondo e' possibile creare una nuova istanza di una variabile che non ha nulla a che spartire (tranne il fatto di avere lo stesso nome) con la corrispondente creata in un blocco piu' esterno. Facciamo un esempio:

BEGIN
  VAR X:INTERO
  Y:INTERO
  X:=0
  Y:=100
  WHILE X<>Y DO
  BEGIN
    X:=X+1
    Y:=Y-1
    IF X=10 THEN BEGIN
                   VAR X:INTERO
                   WHILE X>0 DO BEGIN
                                  Y:=Y-l
                                  X:=X-1
                                END
                 END
  END
  PRINT "HO FINITO ",X,Y
END

All'inizio ci sono le  due dichiarazioni per X e Y di tipo intero. Segue la loro inizializzazione rispettivamente a 0 e a 100. Incontriamo a questo punto un comando while che fa ciclare il blocco seguente fino a quando X e Y non diventano uguali. Figura 3Nel blocco del while, dopo aver incrementayo di 1 la X e di egual misura decrementato la Y, se ha raggiunto il valore 10 si passa al blocco del THEN. Qui troviamo troviamo una dichiarazione di nuova istanza per X che viene inizializzata a 25. E' importante notare che dentro a tale blocco la X di fuori non e' accessibile mentre lo e' la Y che e' stata dichiarata nuovamente. La X esterna, che non e' scomparsa, e' solo disattivata, ritornerA' in vita (e col suo valore 10) una volta usciti dal blocco del THEN. Il resto del programma e' ovvio.

A questo punto qualcuno si chiedera': "Serve tutto quanto?". Si', come era prevedibile. Noi abbiamo fatto l'esempio di una variable: se manipolavamo matrici di 10000 elementi la differenza sarebbe stata piu' sensibile. Immaginiamo che, per un qualsiasi motivo in un punto di un programma ci servono delle nuove matrici per effettuare delle operazioni locali ad un preciso momento dell'algoritmo. Ipotizziamo ancora che tale necessitA' non sempre si presenta, ma e' funzione dei dati di ingresso,. Dichiarare le matrici ausiliarie nel momento in cui verrA' restituito alla loro deallocazione automayica all'uscita del blocco.

Procedure e funzioni

Un altro dei meccanismi di programmazione offerto dai linguaggi algol-like e' la definizione delle procedure e delle funzioni. Sono assimilabili a meccanismi di estensione del linguaggio in quanto una procedura puo' essere vista come un nuovo comando cosi' come per le funzioni definibili. Sia l'une che l'altre, una volta definite, vengono usate come normali statemen e funzioni, come se queste fossero proprie del linguaggio.

Se ad esempio abbiamo la necessita' di un comando, che presi due argomenti ne calcola la somma e stampa il risultato, possiamo definirci la seguente procedura:

PROCEDURE PIPPO (X,Y:INTERO)

BEGIN

  VAR SOMMA: INTERO

  SOMMA:=X+Y

  PRINT SOMMA

END

e da questo momento possiamo usare PIPPO a nostro piacimento, ad esempio PIPPO (4,9),  (28*A, J/2)  o similmente.

X e Y della procedura sono detti parametri formali e effettivamente stanno li' pro forma. Nel senso che servono solo per associare i parametri di ingresso (detti attuali) a qualcosa (i nomi X e Y) che saranno usati all'interno della procedura. Quindi tanto questi, quanto la variabile SOMMA dichiarata all'interno della procedura, una volta terminata l'esecuzione di questa, non esisteranno piu'. Esistono cioe' solo nell'ambiente locale della procedura, che dal canto suo si comporta come un blocco essendo costituita di fatto da un blocco. Analogamente possiamo definirci una funzione che potremo comodamente usare nelle nostre espressioni, come se fosse una funzione offerta direttamente dal linguaggio. A differenza oelle procedure, le funzioni restituiscono un valore e quindi bisogna dichiarare oltre al tipo dei parametri anche il tipo del risultato. Facciamo un esempio: immaginiamo che il nostro linguaggio non dispone della funzione tangente di un angolo, ma solo delle funzioni uguale al seno diviso spesso la tangente non vogliamo ricorrere a scrivere ogni volta seno su coseno. Definiamo la nostra funzione cosi':

FUNCTION TAN (X:REAL): REAL

BEGIN

  TAN:=SIN(X)/COS(X)

END

e potremo usare TAN dove e come vogliamo, naturalmente nel giusto contesto: come espressione che restituice un valore: es. A:=TAN (30), A:= TAN (30) +SIN (45) e cosi' via.

 

Routine e coroutine

Tutti sanno cos'è una subroutine, come si chiama e come, da questa, si torna al programma chiamante. La prima operazione, generalmente gosub o cali ha come parametro il nome o l'indirizzo dove saltare. La seconda operazione, return o similare, si usa generalmente senza specificare altro. Naturalmente le subroutine possono essere tra loro nidificate, nel senso che una subroutine, allo stesso modo del programma principale può a sua volta invocare altre subroutine e via dicendo.

Figure A, B, CIn figura A è mostrato un programma che con CALL «A» invoca la routine A, la quale a sua volta con CALL «B» invoca B. Incontrato il return di B il controllo passa nuovamente a A per poi passare al programma principale dopo il return di questa. In ognuno di questi quattro momenti, indicati con i numeri 1-4 sempre in figura A, anche se non direttamente visibile, è in gioco un altro oggetto, di primaria importanza per il buon funzionamento del meccanismo dei sottoprogramma: lo Stack dei punti di ritorno.

Come funziona una struttura a Stack, ne abbiamo già ampiamente parlato altre volte, quindi passiamo oltre. In tale stack, sono posti come dice il suo nome i punti di ritorno delle subroutine: dove il controllo deve tornare una volta incontrato il return. Tale punto sarà ovviamente l'istruzione seguente il CALL attivante. In figura B è mostrato lo stack dei punti di ritorno nei 4. momenti salienti di cui sopra: in l è posto sullo stack 100 l che è l'indirizzo successivo al CALL «A» del programma principale; in 2 succede esattamente la stessa cosa per il sottoprogramma A che chiama B; in 3, B esegue il return e quindi dallo stack si preleva il 200 l che serve per tornare ad A. Analogamente in 4 per tornare al programma principale.

In figura C è mostrato il funzionamento delle coroutine. Queste, in numero sempre maggiore o uguale a 2 hanno un funzionamento globalmente (le coroutine tutte insieme) simile ad una subroutine ma prese singolarmente sono una cosa ben diversa. Globalmente simili vuoi dire che il programma principale alla stessa stregua di un sottoprogramma esegue un CALL per far partire la prima coroutine. Allo stesso modo, il primo return che si incontra fa tornare il controllo al programma principale. Le coroutine invece, prese singolarmente si invocano l'un l'altra tramite l'istruzione RESUME che ha la particolarità (a differenza della CALL) di far ripartire la coroutine dal punto in cui precedentemente s'era fermata inseguito a un suo RESUME. Per convenzione, una coroutine che non è mai partita, se «riesumata» inizia dalla prima istruzione.

Tornando alla figura C, la sequenza degli eventi mostrati, in ordine cronologico è la seguente: il programma principale invoca A; A, dopo qualche sua istruzione riesuma B (che parte dall'inizio); dopo un po' B riesuma A che riprende dal resume B precedente. E così via fino al RETURN di B che usando anch'esso lo stack dei punti di ritorno fa tornare al programma principale.

Più semplicemente dei sottoprogrammi, per l'implementazione è sufficiente associare ad ogni coroutine una cella di memoria, inizializzata all'indirizzo di partenza della coroutine stessa, nel quale è salvato di volta in volta il proprio indirizzo di riesumazione. Tutto qui.


Impaginato originale...


  Clicca per ingrandire...
Clicca per ingrandire... Clicca per ingrandire...
Clicca per ingrandire...  

Articolo pubblicato su www.digiTANTO.it - per ulteriori informazioni clicca qui