LINGUAGGI

CORSO DI
AMIGA BASIC

Seconda puntata del corso di programmazione sull'AmigaBasic, l'interprete Basic dell'Amiga


di Paolo Russo


   La strutturazione è una componente molto importante di ogni linguaggio e la trattazione di tale argomento, già iniziata in precedenza, viene adesso conclusa con la presentazione di ulteriori informazioni di carattere eminentemente pratico, più che teorico, riguardanti sia le strutture di controllo che i subprogram. Viene poi discusso il problema della gestione della memoria dell'Amiga dal punto di vista del programmatore AmigaBasic; infatti, al contrario della maggior parte delle macchine a otto bit e di molte di quelle a sedici bit, che non soffrono del problema della condivisione delle risorse tra più programmi, l’Amiga possiede un sistema operativo multitasking che consente sì a più di un programma di girare contemporaneamente sulla macchina ma obbliga i programmi stessi ad attenersi a una serie di regole per evitare che due di loro tentino di usufruire della stessa zona di memoria. Protocolli simili esistono anche per quanto riguarda la condivisione della tastiera, del drive e di parecchie altre risorse, ma l'interprete AmigaBasic o il sistema operativo solitamente si occupano autonomamente di questi dettagli senza scomodare il programmatore. La gestione della RAM è invece lasciata agli esseri umani, essendo spesso la quantità di memoria disponibile un fattore critico per il funzionamento di un programma, dal momento che, al contrario di altre risorse come la tastiera e il drive, presenta spesso la seccante tendenza a esaurirsi.

Le strutture di controllo

   L'AmigaBasic ha in comune con il Basic standard il classico e universalmente conosciuto ciclo FOR-NEXT, sul quale non è opportuno soffermarsi a causa della sua eccessiva fama. Il ciclo WHILE-WEND, molto di moda recentemente e spesso impiegato anche a sproposito, è una generalizzazione del ciclo FOR-NEXT dal quale differisce nella condizione di uscita dal loop; mentre il ciclo FOR-NEXT si interrompe solo quando il valore di una certa variabile supera un certo limite, il ciclo WHILE-WEND prosegue fintantoché la condizione (formalmente simile a quella che segue un IF) specificata dopo la parola WHILE si mantiene vera. Ciò significa che la linea WHILE 1 = 1: PRINT "X": WEND scriverà un numero infinito di X mentre la linea WHILE 1 =2: PRINT "X": WEND non ne stamperà nemmeno una. La coppia WHILE-WEND consente anche di emulare quella FOR-NEXT; le seguenti due linee sono infatti equivalenti:

   FOR I = 1 TO 10: NEXT I = 1: WHILE I<=10:
   I=I+1: WEND

   S'intende che il contrario non è possibile: un generico WHILE-WEND non è sostituibile con un FOR-NEXT. Qualcuno si chiederà che scopo abbia tentare di usare una struttura al posto di un'altra. Naturalmente, come risulta evidente dall'esempio riportato poco sopra, il rimpiazzamento di un ciclo FOR-NEXT con uno WHILE-WEND non è mai conveniente in termini sia di praticità che di leggibilità, ma un rispettabile numero di persone in vena di semplificazioni ritengono che due strutture al posto di una siano ridondanti ed evitano sistematicamente il FOR-NEXT nei loro programmi, tentando nel contempo con tutte le loro energie di convincere chiunque altro che ciò sia indice di buona programmazione. Il lettore è consigliato di non lasciarsi trasportare ciecamente dai flussi e riflussi della moda, esistenti nella programmazione come in molte altre attività umane, ed è altresì vivamente incoraggiato alla creazione di uno stile personale, soprattutto se non desidera che l'eccitante attività programmatoria si trasformi rapidamente in una grigia routine.
   Al classico IF-THEN(-ELSE) è stato affiancato il più flessibile IF-THEN (-ELSEIFTHEN) ... (-ELSEIF-THEN) (-ELSE) -END IF che consente di eseguire in maniera condizionata interi blocchi di linee alla volta, che possono a loro volta contenere altri IF. L'interprete identifica la variante di IF (classica o moderna) che il programmatore intende usare basandosi sulla presenza o meno di istruzioni in linea subito dopo il THEN; se infatti tale parola chiave è seguita da una o più istruzioni l’interprete decide che si tratta di un IF classico, a linea singola; se al contrario dopo il THEN non si scrive niente deve evidentemente trattarsi della variante evoluta multilinea e l’AmigaBasic parte dal presupposto che le linee successive debbano essere eseguite solo se la condizione specificata risulta vera. Naturalmente è necessaria l'esistenza di una parola chiave per contrassegnare la fine del blocco di linee da eseguire condizionatamente; tale parola, che deve occupare un'intera linea, è END IF. La parola ELSE, che esige anch'essa di non essere preceduta o seguita da alcunché, marca invece l'inizio delle linee che devono essere eseguite solo se la condizione è falsa. Al contrario delle parole IF, THEN ed END IF, la cui presenza è obbligatoria, l'uso di un blocco introdotto da ELSE è opzionale, e altrettanto può dirsi per ELSEIF, utile in caso di scelte multiple. La parola ELSEIF, che richiede una condizione e un THEN dopo di sè, introduce un blocco di istruzioni che verranno eseguite solo se l'ultima condizione specificata è vera e tutte le precedenti sono false. Eccone un esempio:

   IF A=1 THEN
       PRINT "UNO"
   ELSEIF A= 2 THEN
       PRINT "DUE"
   ELSEIF A=3 THEN
       PRINT "TRE"
   ELSE
       PRINT "NON È UNO, DUE O TRE"
   END IF

   L'indentazione, da qualcuno chiamata dentellatura, ossia l'abitudine di lasciare all'inizio di ogni riga uno spazio variabile in modo da evidenziare i blocchi, non è obbligatoria ma molti la consigliano per motivi stilistici, trovandola evidentemente molto estetica o particolarmente giovevole alla chiarezza del listato. Purtroppo quando si esce dall'ambito dei programmi banali accade che all'interno di ogni singola procedura il numero di IF, FOR e WHILE concatenati sia solitamente abbastanza elevato da far sì che alcune parti del listato dimostrino una spiccata tendenza a uscire dall'area visibile dello schermo a causa della spaziatura iniziale.
   L'AmigaBasic possiede anche il GOTO, ma tale direttiva è stata implementata nella presente release dell'interprete in una forma non troppo flessibile; è infatti proibito saltare dentro e fuori da una struttura tramite GOTO, cosa invece consentita da qualche altro computer. Non è quindi possibile saltar fuori neppure da un banalissimo ciclo FOR-NEXT prima del suo esaurimento: se si desidera farlo occorre assegnare il valore limite alla variabile indice e saltare al NEXT, cosa questa fattibile ma indubbiamente alquanto scomoda.

I dati dei subprogram

   È stato detto nella scorsa puntata che le variabili di un subprogram esistono solo al suo interno, eccezion fatta per quelle dichiarate come SHARED che sono condivise con il programma principale. La parola STATIC che compare nella prima linea di ogni subprogram ha il seguente significato: tutte le variabili locali al subprogram sono statiche, ossia il loro contenuto non viene perso tra una chiamata e l'altra del subprogram stesso.
   In realtà la presenza della parola STATIC nella definizione di un subprogram è obbligatoria e ciò significa che L’AmigaBasic non supporta le variabili locali non statiche. Quest'ultimo tipo di variabile è utile per limitare l'occupazione di memoria e per implementare algoritmi ricorsivi, cioè per consentire che un subprogram possa richiamarsi da solo; ciò in AmigaBasic non è quindi possibile, a meno di ideare un meccanismo che salvi le variabili prima di ogni chiamata. Esiste tuttavia un ulteriore inconveniente legato alla staticità delle variabili: com'è possibile utilizzare degli array locali in un subprogram? Se dimensioniamo l’array in questione nel programma principale perdiamo il vantaggio della località, oltre a essere costretti a dichiararlo SHARED; se il DIM compare invece all'inizio del subprogram i guai iniziano quando il suddetto viene richiamato per la seconda volta e l’interprete si trova nella seccante situazione di dover dimensionare un array già esistente. Ciò è illegale in AmigaBasic e provoca il blocco del programma con segnalazione di errore. Come risolvere questo problema?
   Sfogliando il manuale qualcuno avrà notato l'esistenza di un utile comando chiamato ERASE che cancella un array eliminandolo dalla memoria come se non fosse mai esistito. Si può quindi pensare di inserire questo comando alla fine di un subprogram in modo che alla successiva chiamata l’array, assente, possa essere ridimensionato. Per qualche strano motivo questo trucco non sembra funzionare molto bene; probabilmente il comando ERASE è stato creato appositamente per gli array globali e non funziona sempre su quelli locali. Comunque, anche se funzionasse regolarmente sarebbe un metodo troppo inefficiente, in quanto la cancellazione e successivo dimensionamento sono operazioni alquanto lente, ed eseguirle ogni volta che il subprogram viene richiamato non sembra essere la soluzione migliore. Un trucco più valido consiste nell'inserzione all'inizio del subprogram di una linea come la seguente:

IF FLAG=0 THEN FLAG=1:DIM AR(100)

   La prima volta che il subprogram viene richiamato le sue variabili non esistono ancora; FLAG vale quindi zero, come tutte le variabili non ancora inizializzate. Alla suddetta variabile viene quindi assegnato il valore uno e l’array viene dimensionato. A ogni successiva chiamata del subprogram la condizione espressa dopo l’IF risulterà falsa e non sarà quindi effettuato alcun tentativo di dimensionare nuovamente l’array. Se poi all'interno del programma principale viene eseguito un CLEAR tutte le variabili spariscono, array compreso, ma anche FLAG torna a zero e il trucco continua a funzionare.

Lo statement CLEAR

   L'istruzione CLEAR in ogni versione di Basic provoca la cancellazione di tutte le variabili, ma in alcuni dialetti essa assume un significato più ampio. Nel caso in questione la suddetta istruzione consente di cambiare il modo in cui la RAM viene ripartita tra le sue tre aree fondamentali: lo heap, lo stack e il segmento del Basic. Lo heap (cumulo) è una sorta di magazzino pubblico di memoria inutilizzata al quale tutti i programmi attingono secondo le loro necessità, dopo aver rivolto una regolare richiesta al sistema operativo (a Exec, per la precisione); ormai nemmeno i programmi per computer riescono a salvarsi dalla burocrazia. Se non altro Exec non insabbia mai le pratiche e si preoccupa di sbrigarle nel giro di qualche millisecondo, respingendole, in un tempo altrettanto breve, solo se la memoria è ormai esaurita o non possiede le caratteristiche richieste: un traguardo di efficienza del tutto fantascientifico per la burocrazia italiana. Purtroppo certe richieste che devono essere rivolte alle sezioni competenti del sistema operativo per ottenere determinati risultati prevedono la compilazione, da parte del programma richiedente, di un vero e proprio modulo di complessità tale da far invidia al tristemente noto 740; ma di questo si tratterà meglio in futuro. Il numero che compare nella barra superiore dello Workbench indica appunto la quantità di memoria ancora disponibile nello heap. Ogni programma che gira sull'Amiga possiede uno stack (pila, catasta) il cui scopo è la memorizzazione temporanea di indirizzi e variabili locali e la cui dimensione varia solitamente tra uno e quattro kappa di RAM; l’interprete AmigaBasic è esso stesso un programma e possiede di conseguenza uno stack. Il segmento Basic è quella zona di memoria che l’interprete ha sottratto allo heap per destinarla alla memorizzazione del programma Basic e delle sue variabili. La sintassi completa del comando è CLEAR (,segmento (,stack)), dove le parentesi indicano, come già in precedenza, un parametro che può essere omesso.
   L'esperienza insegna che praticamente nessuno trova utile alterare la dimensione dello stack, mentre molti programmatori desiderano aumentare l'esiguo spazio a disposizione del Basic, che normalmente ammonta a circa 25K. Bisogna innanzitutto premettere che non è buona norma assegnare troppa RAM al segmento lasciando al verde lo heap, non solo perché risulterebbe impossibile il funzionamento in multitasking di un qualunque altro programma assieme all'interprete, ma anche perché determinate azioni intraprese da un programma Basic assorbono memoria dallo heap; il comando SCREEN per esempio definisce un nuovo schermo e preleva dallo heap la memoria necessaria alla relativa pagina grafica. Quando inoltre vengono eseguite certe operazioni, come lo spostamento di una finestra con il mouse, il computer necessita temporaneamente di una certa quantità di RAM per portare a termine il compito richiesto e se non la trova possono accadere stranezze di vario genere.
   Il modo più semplice per riservare al segmento una certa quantità di RAM consiste nell'eseguire uno statement come CLEAR , 100000 (se si desiderano circa 100K; notate comunque la curiosa quanto obbligatoria presenza della virgola). Questo metodo presenta qualche inconveniente; il primo consiste nell'impossibilità di allocare ripetutamente più di metà (nel migliore dei casi) della memoria disponibile. Supponiamo per esempio che su di un Amiga inespanso si desideri portare il segmento a 120K; basta digitare in modo diretto CLEAR ,120000 e si è poi liberi di creare programmi con array di dimensioni veramente notevoli. Quando poi il programma viene salvato e in seguito ricaricato, magari dopo un reset, si scoprirà che non funziona più perché l'effetto del comando CLEAR è svanito, dal momento che anche l’interprete è stato ricaricato da disco, e non c'è più posto per le variabili. Evidentemente è opportuno inserire il CLEAR in testa al programma stesso, in modo che venga eseguito a ogni caricamento; così facendo, tuttavia, tale comando verrà eseguito anche a ogni run e ciò crea un altro problema, connesso al modo in cui l’interprete porta a termine un CLEAR. L'AmigaBasic richiederà i 120K prima di restituire i vecchi 25K allo heap, altrimenti il programma Basic verrebbe perso per strada. Per un attimo quindi l’interprete possiederà 145K. Non c'è niente di male in ciò, ma al successivo run l’AmigaBasic tenterà di ottenere altri 120K prima di mollare i vecchi 120, con esito infelice non essendo disponibili 240K di memoria. Si può allora ricorrere a un doppio CLEAR; supponendo che il programma in sè (variabili escluse) non richieda più di 10K, si può eseguire un CLEAR ,10000:CLEAR ,120000. In tal modo la massima occupazione di RAM in ogni dato istante non supera 130K. Esiste poi un ulteriore inconveniente: se l'abbondanza di memoria è necessaria non solo per le variabili, ma per il programma stesso, risulta impossibile caricarlo in memoria prima di aver eseguito il CLEAR, che a sua volta entrerebbe in azione solo dopo il caricamento: il classico serpente che si morde la coda. L'unica via di uscita consiste nella creazione di un programma di lancio come il seguente:

   CLEAR ,5000
   CLEAR ,120000
   LOAD "nomeprogramma" ,R

   Questo microscopico programmino, se lanciato in esecuzione, riserva al Basic la quantità desiderata di RAM e subito dopo carica il programma vero e proprio, attivandolo al termine del caricamento (l’opzione R significa Run). Naturalmente non è piu necessario includere in quest'ultimo programma istruzioni per allocare memoria, essendo questo compito già svolto dal programma di lancio.
   La funzione FRE(x) consente di conoscere in ogni momento il modo in cui la RAM è ripartita; il classico FRE(0) fornisce la quantità di memoria ancora libera all'interno del segmento Basic, FRE(-1) la dimensione dello heap, FRE(-2) la quantità di RAM nello stack che non è mai stata usata. Ogni volta che la funzione FRE viene invocata l’interprete esegue il cosiddetto garbage collecting (alla lettera, raccolta di immondizia); tale operazione riordina le stringhe presenti in memoria in modo da riutilizzare gli interstizi che si formano di solito durante la loro manipolazione.
   Una piccola curiosità: anche il comando CLEAR del vecchio ZX Spectrum ha la capacità di variare la porzione di RAM riservata al Basic! Chissà quanti altri computer possiedono questa caratteristica così assolutamente non standard associata al comando CLEAR?