La Torre di BaBasic - 8
La Torre di BaBasic - 8
Noi ci troviamo nella stessa situazione quando vogliamo dare un nome ai linguaggi di programmazione: ormai sono troppi e troppo diversi. Possiamo però studiare una famiglia di linguaggi davvero speciali: i linguaggi funzionali.
Nelle scorse sette puntate abbiamo passato in rassegna molti aspetti interessanti della programmazione, e nel farlo abbiamo tenuto d'occhio molti diversi linguaggi, dall'umile Assembly alla superba Ada. E tuttavia ci siamo presto resi conto che le fondamenta logiche di tutti questi linguaggi, vecchi e nuovi, potenti e deboli, sono quasi identiche: infatti abbiamo potuto esprimere gli stessi concetti in uno qualunque di questi, scegliendolo per la maggior chiarezza concettuale o semplicemente a capriccio.
A ben guardare, infatti, sotto a tutti i linguaggi di programmazione sta la stessa entità: la macchina fisica conosciuta come processore. Come probabilmente saprete, il processore è un po' il cervello del computer: è quella componente che esegue tutti i calcoli. E tutti i processori esistenti al mondo - con poche eccezioni che vedremo tra poco - sia quelli potentissimi, raffreddati ad elio liquido, che occupano intere piastre elettroniche e governato i più grandi computer, sia il piccolo microprocessore, dove tutti i componenti sono radunati in un singolo componente, che risiede nel vostro personal computer, tutti questi processori hanno una natura estremamente simile, poichè derivano dal modello concettuale creato decadi fa da Von Neumann, forse il più brillante tra gli scienziati che si sono occupati di informatica.
La Macchina di Von Neumann, pur essendo un modello teorico, è realizzata fisicamente nei processori. I processori esistenti sono ben poco differenti l'uno dall'altro: l'umile 6502, capostipite dei microprocessori, che animava i primi personal computer di Commodore ed Apple, non ha quasi nulla da invidiare ai più blasonati nipoti di oggi. Certo, gli 80386 o i 68030 che sfoggiano i computer di oggi compiono anche 25 operazioni nel tempo che il buon vecchio 6502 richiedeva per una sola, ma si tratta dello stesso tipo di operazioni: muovi una cella di memoria qui, sommane due là, esegui quel tale sottoprogramma. Le differenze qualitative di capacità ci sono, è vero, ma sono relativamente minori: per esempio, il 6502 era in grado di eseguire solo somme e sottrazioni di numeri interi, mentre i suoi nipoti arrivano sino ad eseguire moltiplicazioni e divisioni. I processori che animano i mini e maxi computer arrivano a compiere operazioni su stringhe di lunghezza variabile, e questo è tutto.
Rispecchiando la natura dei processori, tutti i linguaggi condividono certe caratteristiche: proviamo ad elencarne qualcuna.
Innanzitutto c'è il concetto di variabile, che abbiamo sviscerato nella seconda e terza puntata di questa serie. Una variabile altro non è che la sofisticazione del concetto di locazione di memoria, quel componente del computer che può conservare un valore numerico sinchè la macchina non venga spenta o sinchè un nuovo valore viene sovrimposto al vecchio.
Abbiamo poi il concetto di sottoprogramma, come visto nella prima puntata: una procedura, una funzione e persino per molti versi un task eseguito in parallelo, sono sostanzialmente dei sottoprogrammi: la loro esistenza è uno sviluppo della subroutine che il processore sa eseguire; con una apposita istrzuzione si comanda al processore di passare ad eseguire una serie di istruzioni da un'altra parte del programma, per poi ritornare nel punto corrente quando questa ha terminato.
Possiamo infine menzionare le strutture di controllo: qualsiasi programma, sia esso espresso in un linguaggio ignobile come il Cobol o ricco e moderno come Ada e Chill, tende a svolgere il suo compito eseguendo certi gruppi di istruzioni per un grande numero di volte. Ripeti queste istruzioni finchè quella condizione non si avvera, ecco il principio su cui si fondano tutti i programmi di questo mondo, dai più semplici videogiochi sparaspara ai sofisticati programmi per uso militare che ogni tanto buttano giù per errore un aereoplano. Anche le strutture di controllo discendono da semplici operazioni che i processori sanno compiere, e cioé paragonare due numeri e quindi procedere ad eseguire questo o quel gruppo di operazioni, come abbiamo visto nella quinta puntata.
Ci siamo interessati di tutte queste caratteristiche comuni dei più diffusi linguaggi di programmazione in questi ultimi mesi. E abbiamo sinora ignorato una intera famiglia di linguaggi che non si comportano in questo modo: i cosiddetti linguaggi funzionali.
Storicamente, i linguaggi funzionali nascono dal desiderio di alcuni matematici di possedere un linguaggio di programmazione che rispondesse direttamente alla logica matematica. In seguito si è aggiunta una considerazione differente, e commercialmente più significativa, che ha dato nuovo impulso alla ricerca di nuovi e differenti linguaggi di programmazione: con i linguaggi tradizionali è estremamente difficile raggiungere una prova formale di correttezza di un programma. Con quest'ultima frase, interamente composta da endecasillabi, intendiamo dire che, come tutti i programmatori novellini o provetti ben sanno, non è possibile stabilire d'ufficio se un programma sia corretto o contenga degli errori. Gli errori si scoprono invece facendo funzionare più e più volte il programma, cercando di provare tutte le situazioni possibili; e dato che oggigiorno i programmi si fanno sempre più complessi e potenti, è divenuto uso comune per ogni casa produttrice di software assumere interi gruppi di tecnici interamente addetti alla prova dei programma quasi finiti, i cosiddetti beta tester4.
Esistono in verità dei metodi per esaminare formalmente, con certi complessi strumenti matemaici, un programma, per verificare che esso effettivamente faccia quel che dovrebbe fare: il problema è che per stabilire la correttezza di dieci righe di programma sono necessarie almeno tre o quattro ore di calcoli. E, per di più, non è neppure possibile applicare questi metodi a tutte le istruzioni presenti in un linguaggio: un semplice costrutto come if..then manda questi metodi matematici in scompiglio, mentre un ancora più semplice goto li costringe alla resa immediata.
Certo, sarebbe bello possedere un metodo completo e veloce che possa scoprire gli errori in un programma: se questo fosse possibile, potremmo scrivere un programma che applichi questo metodo agli altri programmi, e la parte più lunga e noiosa della creazione del nuovo software, il debugging, sarebbe eliminata dal processo di produzione dei programmi, e i beta tester potrebbero passare allo stesso reame dove dimorano i cavalieri erranti, gli scribi e le mondine.
Se ci rivolgiamo ai linguaggi funzionali anzichè ai più tradizionali linguaggi di programmazione (che vengono per contrapposizione indicati come linguaggi procedurali), questo desiderio passa dalla sfera dell'impossibile alla sfera del possibile ma non ancora scoperto. E questo semplice fatto è sufficiente a darci un certo interesse: diamo dunque un'occhiata a questi misteriosi linguaggi funzionali, ma senza dimenticare che i linguaggi tradizionali sono di gran lunga i più diffusi, i più potenti e i più efficienti (proprio perchè rispecchiano la natura fisica della macchina che li fa funzionare).
Il primo concetto di cui ci sbarazziamo è quello di variabile. La variabile nei linguaggi di programmazione è estremamente pericolosa, poichè non rispetta le regole matematiche. Provate ad esaminare le due formule seguenti:
- y1 := x + f(x)
- y2 := f(x) + x
Function f (var x: integer): integer;
begin
x := x + 1;
f := x
end;
Figura 1: La funzione dei linguaggi di programmazione non rispetta neppure le più elementari caratteristiche delle funzioni matematiche.
Con pochi istanti di riflessione ci rendiamo conto che qualcosa non va come dovrebbe. Poniamo che x valga 3: nel caso della prima formula otterremo il risultato di 7, mentre nel secondo caso il risultato sarà 8. La mia vecchia insegnante di matematica del liceo, scoprendo a questo punto che l'addizione non è commutativa, alzerebbe gli occhi al cielo ululando: "Ma dov'è finito il buonsenso?"
Nei linguaggi funzionali, dove la logica sottostante è quella matematica, le funzioni devono essere funzioni nel senso matematico; pertanto, sia fatta sommaria giustizia delle variabili. Spostate più avanti quella ghigliottina, perché possano vedere anche i bambini! Zac!
Molti lettori probabilmente non riusciranno ad immaginarsi come si possa programmare senza le variabili, senza le istruzioni di controllo, senza i cicli e vincolando le funzioni ad essere funzioni matematiche. In effetti, il primo impatto con quello che va sotto il nome di programma in un linguaggio funzionale è spesso drammatico.
Il primo linguaggio funzionale ad essere creato è stato il Lisp, un linguaggio oggi molto utilizzato nel campo dell'intelligenza artificiale. Ma il Lisp che oggi viene usato si è evoluto, ed è un ibrido mezzo funzionale e mezzo procedurale: ci sono le variabili e ci sono i sottoprogrammi.
Un altro vecchio linguaggio che ha molto in comune con i linguaggi procedurali è lo Apl (il cui nome non molto originale è la sigla di A Programming Language, cioé "un linguaggio di programmazione"): anch'esso consente l'uso di variabili, ma è per il resto un perfetto linguaggio funzionale.
Osserviamo un famoso, classicissimo esempio di programmazione Apl per avvicinarci alla programmazione funzionale, e per farlo introduciamo alcuni concetti matematici, nonchè parecchi simboli greci che faranno impazzire il nostro povero caporedattore e tutto il resto della redazione (hi hi hi).
L'operatore | viene utilizzato per ricavare il modulo di un numero. Si tratta del resto di una divisione tra interi: per esempio 7 | 3 è uguale ad 1, perché 7 : 3 = 2 con il resto di 1.
L'operatore iota, indicato dal simbolo i, serve a ottenere i primi numeri interi: per esempio, i 5 produce la sequenza 1 2 3 4 5.
Il normale prodotto si indica con il punto, e perciò 5 . 3 = 15
Con un piccolo pallino, °, invece, si indica il prodotto esterno. Si tratta di una forma funzionale, cioé una funzione che non si applica ai numeri ma ad altre funzioni (si, lo sappiamo, voi credevate che la matematica del liceo fosse difficile, prima di prendere in mano questa copia di Bit. Solo un po' di pazienza ancora). Una forma funzionale si applica, dicevamo, a una funzione: per esempio al prodotto, scrivendo °.
Per capirci, potete ottenere la tavola pitagorica scrivendo (i 10) °. (i 10) poiché ciascuna delle due espressioni tra parentesi genera i numeri dall'uno al dieci. Il prodotto viene applicato dalla forma funzionale a tutte le coppie di numeri, generando così i cento numeri che formano la tavola pitagorica.
Abbiamo poi la forma funzionale di inserimento, caratterizzata dalla sbarra /. Essa viene usata per applicare consecutivamente una funzione a una lista di argomenti: per esempio, noi sappiamo che la somma è definita tra due numeri, ragion per cui 2 + 2 = 4, ma applicando la forma funzionale possiamo estendere la validità della funzione somma ad un numero qualsiasi di argomenti, e avremo dunque:
(+ / (4 7 6 1)) = 18
o anche
(+ / (i 10)) = 55
perchè la somma dei numeri da 1 a 10 vale appunto 55.
I tre lettori che mi hanno seguito sin qui non abbandonino ora la partita: siamo arrivati all'esempio che avevamo promesso. Si tratta di una formula scritta in Apl che permette di ricavare i numeri primi. Ricordiamo la definizione di numero primo: un numero intero divisibile esattamente solo per se stesso o per 1.
2 = ( + / 0 = (i 1000 ° | . i 1000))) / i 1000.
Questa formula, che è all'occhio inesperto solo leggermente meno comprensibile di un brano del Corano in arabo con note a pié di pagina in sanscrito, funziona come segue: vengono generati i primi mille numeri, e divisi tra di loro sino ad ottenere i resti della divisione - questo accade nella parte centrale della formula. Se il resto della divisione tra due numeri, x : y, è zero, il numero x non è primo; pertanto sommiamo il numero di resti non uguali a zero, e se ne troviamo soltanto due abbiamo trovato un numero primo. Questo è garantito dalla definizione di numero primo.
Lasciamo i nostri lettori mentre un lieve filo di fumo esce dal loro orecchio sinistro, suscitando la comprensibile preoccupazione dei familiari. Nella prossima e ultima puntata vedremo come viene creato e come funziona il compilatore di un linguaggio di programmazione.