Give me six hours to chop down a tree and I will spend the first four sharpening the ax
(Abraham Lincoln)
Photo by Markus Spiske on Unsplash
Credo di aver scritto il mio primo programma intorno al 1977. In Fortran. Non so come fossi finito a fare informatica, ricordo di aver comprato un numero di “Nuova Elettronica”, anche questo chissà perché, che parlava di microprocessori. Avevo sfogliato un po’ la rivista, senza capirci praticamente niente, ma mi era piaciuto, credo, il tono di trionfo, l’entusiasmo per qualcosa di importante che stava succedendo e di cui loro, quelli che scrivevano gli articoli, facevano parte.
Durante il corso di macchine (Teoria e applicazioni delle macchine calcolatrici, come dire non sapevamo proprio come chiamarlo questo corso, si poteva accedere al centro di calcolo e far girare i propri programmi. Era in via S. Massimo a Torino, non so se c’è ancora. Il centro ospitava un grosso calcolatore IBM, erano ancora grossi come caldaie condominiali all’epoca, e una sala con delle macchine per perforare schede. Ti davano un pacco di schede vergini. Facevi la coda per accedere a queste strane macchine da scrivere e cominciavi a copiare il programma che ti eri scritto a casa. Una linea per scheda. Ne usciva un pacco di schede che, dopo un’altra coda, consegnavi a uno studente che faceva girare il tuo job sul mainframe. Dopo qualche tempo, anche ore a volte, tornavi e ti consegnavano un tabulato col risultato del tuo programma. In genere era una pagina con scritto a caratteri cubitali “JOB FAILED”, che era il modo del computer di dirti che avevi sbagliato qualcosa. C’era anche qualche informazione per guidarti nel capire cos’è che non andava.
Per riuscire a far girare quel programma credo di aver impiegato diverse settimane, ma alla fine lo studente mi consegnò, al posto del solito foglio striminzito un bel pacco di fogli. In ogni foglio c’era il disegno di una scacchiera. L’insieme dei fogli conteneva la progressione delle mosse che doveva fare un cavallo per occupare tutte le posizioni della scacchiera senza mai ripassare dallo stesso posto.
Era un esercizio trovato su un libro che insegnava quattro linguaggi contemporaneamente, per mostrare le differenze e le comunalità tra di essi. Non so perché mi piacque quell’esercizio, ancora adesso penso di aver avuto una fortuna incredibile a farlo funzionare. I linguaggi che insegnava il libro erano Cobol, Fortran, Algol e PL/I.
Io il Fortran e il Lisp siamo praticamente coetanei. Beh, loro sono un pelo più giovani, sono nati un anno o due dopo. Ma il Lisp l’ho conosciuto parecchio dopo, era già scattato il nuovo millennio. E’ strano come spesso siamo circondati da cose belle che non vediamo. Magari nessuno ce ne parla, o quelli che ce ne parlano ci sembrano strani e non gli diamo retta, e invece, a volte, hanno scoperto un tesoro. Credo valga per molte cose, tra cui l’arte e la meditazione. Certo anche per il Lisp.
Così tanti
C’è qualcosa di affascinante nel numero di linguaggi esistenti. Parlo dei linguaggi naturali, quelli parlati e ascoltati da persone. Sembra che la bibbia sia stata tradotta in più di 2500 lingue e che da un censimento il numero dei linguaggi superi i 6000.
Mi sono chiesto spesso perché nasca un nuovo linguaggio. Se guardo alla mia personale esperienza di come il linguaggio, l’italiano in questo caso, evolve, quello che noto è che c’è una spinta da parte di gruppi di persone a differenziare il linguaggio per farne strumento di appartenenza al gruppo (“parlo come voi quindi faccio parte del vostro ambiente”). Il linguaggio quindi evolve assieme alla nascita di nuove formazioni, e finisce per assorbire e trasportare le idee di questi conglomerati. La storia ci mette del suo, e così pure fa la geografia: man mano che l’uomo ha popolato la terra si sono creati sicuramente gruppi più o meno isolati, che hanno fatto evolvere il loro modo di esprimersi creando parole per oggetti o eventi che non esistevano nel linguaggio/gruppo di origine.
Il linguaggio contiene la cultura dei gruppi. La difficoltà di imparare un’altra lingua è in gran parte la novità, la differenza, non tanto nei termini usati, ma nel vissuto delle popolazioni che hanno sviluppato quella lingua. La loro lingua trasporta le sfide che hanno affrontato e il modo in cui le hanno risolte.
Le macchine e gli uomini
L’intuizione evangelica della Parola che crea/trasforma il mondo è applicabile non solo alla parola di Dio, ma anche ai linguaggi naturali, e diventa ancora più vera applicata ai linguaggi di programmazione, Parole che muovono cose, che le producono, che creano immagini, musica, che interpretano e predicono il comportamento umano.
Anche i linguaggi di programmazione sono tanti, al momento più di 700. Se pensiamo che sono nati nell’arco di poco più di 50 anni è davvero notevole.
Le ragioni per cui nascono credo siano simili a quelli visti sopra: c’è certamente voglia di distinguersi, forse qualche influenza estetica, voglia di appartenere a qualche gruppo, sicuramente qualche aspetto economico, ma in sostanza i linguaggi nascono perché si trovano modi sempre migliori di risolvere i problemi. Qui la geografia reale ha poco impatto, internet ormai rende il mondo un posto che si attraversa in un attimo/click, ma ci sono nuove geografie che plasmano le differenze nel modo di esprimersi: i vari campi di specializzazione diventano ogni giorno di più continenti che si separano. Chi scrive programmi per applicazioni di commercio elettronico sul web ha problematiche completamente diverse da chi programma dispositivi medici o sistemi operativi o controlli industriali. Le tecnologie di base tentano di restare comuni, la maggior parte dei linguaggi dichiara di essere general purpose, ma quanto meno il tempo di adozione delle novità è molto diverso nei vari settori.
I linguaggi di programmazione non servono tanto a far parlare gli uomini con le macchine, questo ormai è diventato quasi un effetto collaterale. Questi linguaggi servono a parlare in modo non ambiguo tra uomini, servono a scambiarsi idee, soluzioni ai problemi. Servono al singolo a ricordarsi come ha risolto un certo problema in passato. Servono a fare in modo che una persona possa collaborare con un’altra o continuare il suo lavoro, permettono a gruppi di persone di dividersi il lavoro e assemblare a tempo debito i vari frammenti, servono a qualcuno per verificare il lavoro di un altro.
Secondo me non è lontano il tempo in cui si useranno questi linguaggi non tanto per dire alle macchine cosa fare, ma per produrre e conservare conoscenza in modo non ambiguo. La matematica, la musica e la scienza lo fanno da tempo, ma mi stupisco sempre quando leggo il testo di una legge espresso in un linguaggio comunque specialistico e per addetto ai lavori, ma così ambiguo da necessitare di stuoli di avvocati per essere interpretato. Credo che presto vedremo i linguaggi di programmazione invadere campi come la legge, la stesura dei contratti (le blockchain ci costringeranno a brevissimo a questo), le ricette di cucina e la medicina.
Cartoni animati, flipper e insaccatori di salsicce
Dicevamo, modi diversi di risolvere i problemi. Ma il problema di fondo è uno: manipolare la complessità. In genere un programma ha un fine complesso, un obiettivo unico ma scomponibile nella somma di particolari più piccoli: le informazioni anagrafiche nel profilo di un utente, la sequenza di movimenti di un robot, le informazioni di mille sensori nei dispositivi che tengono in volo un aereo. Il programma, in definitiva, è la somma di azioni applicate a questi singoli particolari, ma queste micro azioni nel loro insieme devono tendere al fine principale, e, spesso, è un’arte da giocolieri. La storia dei linguaggi di programmazione è la storia del progressivo perfezionamento di questa capacità di governare la complessità con astrazioni sempre più ricche.
Un conto è dire “procedi per dieci metri, ripeti per 90 volte: – curva di un grado e procedi di 15 centimetri-”, un altro è dire “gira a destra con un raggio di venti metri”. Le astrazioni si costruiscono una sull’altra, come un castello di carte. Un programma che identifica il percorso migliore tra due località potrà fare uso delle astrazioni “gira a sinistra”, “gira a destra” per realizzare l’astrazione “vai da questo posto a quell’altro” e uno che programma le consegne di un corriere userà quest’ultima astrazione e così via.
Photo by Denise Jans on Unsplash
Questo modo di gestire la complessità elencando azioni da fare 1 assomiglia un po’ al creare un film di animazione o alla sceneggiatura di un film. E’ il modo naturale in cui un computer si lascia programmare ed è stato il primo e più diffuso (lo è tuttora) modo di farlo. Tutte i linguaggi che abbracciano questo tipo di soluzione si rifanno alle teorie di un matematico inglese, Alan Turing (quello del film “The Imitation Game”), che ha immaginato un ipotetico automa (la macchina di Turing, appunto) in grado di risolvere, potenzialmente, qualsiasi problema di calcolo. I linguaggi di programmazione vengono classificati “Turing complete” quando possono essere usati per affrontare qualsiasi problema.
Sulla strada del creare astrazioni come ho accennato sopra, per composizione di astrazioni più elementari, non si va molto avanti. E’ sempre possibile “comporre” piccoli mattoncini Lego in mattoni più grandi ovviamente, il gioco che non riesce, praticamente mai, è di poter riutilizzare questi mattoni più grandi in altri contesti.
Negli anni novanta questa ansia di riuso ha portato alla nascita di altri paradigmi, altri modi di affrontare il problema. Qualcuno ha provato a immaginare il problema da risolvere come collezione di piccole entità (non si è trovato nome migliore di oggetti, e il paradigma lo si è chiamato object orented) ognuna delle quali si comportava come un micro calcolatore specializzato per un particolare scopo, possedeva i suoi dati, il suo carattere (un suo modo di reagire agli eventi) e colloquiava col mondo esterno inviando e ricevendo messaggi. Si può immaginare la cosa come costruire un flipper: si piazzano i vari elementi: funghetti, trappole, barriere, insegnando loro cosa fare quando vengono urtati, si lancia la pallina e il gioco è fatto.
E’ sembrata per un po’ la panacea di tutti i problemi e per un bel periodo sembrava impensabile che nascesse un nuovo linguaggio che non supportasse questo modo di lavorare. Oggi l’idea viene ridimensionata perché si è messo a fuoco che ciò che rende davvero difficile creare programmi robusti (un programma che non lo è non serve a niente) è la presenza di uno stato. Un programma imperativo, o a oggetti non si comporta in maniera ripetibile: a fronte degli stessi stimoli può produrre risultati diversi a seconda del suo stato interno, che è a sua volta modificato dalle azioni precedenti. Questo rende il programma, a meno che sia davvero elementare, impossibile da provare nella sua completezza (la casistica diventa infinita) e spesso anche impossibile da pensare, da comprendere in modo logico.
A tutto questo ha dato risposta un altro matematico (ha risposto in anticipo in effetti, visto che è contemporaneo di Turing), di nome Alonzo Church, che ha inventato un modo di programmare da matematico. L’implementazione più famosa delle sue idee è apparsa nel linguaggio Lisp (1958), ed oggi il suo modo di risolvere i problemi viene chiamato programmazione funzionale, tra le altre cose è Turing complete 😉
Photo by Clem Onojeghuo on Unsplash
Una funzione in senso matematico la possiamo immaginare come un tritacarne, o una macchina per fare le salsicce: inserisco ingredienti da una parte, giro la manovella, e dall’altra esce il prodotto. E funziona sempre nello stesso modo: se inserisco gli stessi ingredienti e giro la manovella lo stesso numero di volte esce sempre lo stesso prodotto. Questo, come ho detto, non è vero per le funzioni dei linguaggi imperativi: rimane sempre nella macchina un po’ della salsiccia di prima, che inquina la successiva.
Queste funzioni senza effetti collaterali si chiamano pure functions. Da notare che una funzione pura non è legata neanche al tempo, non può dire ad esempio “aspetto due secondi e poi faccio questo”, non c’è il poi e non c’è nemmeno il faccio. C’è solo “con questi valori di ingresso il risultato sono questi altri”, una dichiarazione senza tempo, non un’azione.
Con queste premesse è impossibile produrre un programma che faccia qualcosa nel mondo reale, per cui i programmi funzionali sono in genere divisi in due strati: uno che traduce gli eventi del mondo esterno (anche il tempo) in puri dati che vengono dati in pasto allo strato di funzioni pure, che esegue i calcoli e passa indietro i risultati alla parte reale. Il vantaggio è che la parte non pura, può essere ridotta al minimo e convenientemente testata, la parte pura, generalmente più complessa, è immensamente più facile da provare e da pensare. Oggi nessun linguaggio moderno non permette almeno una minima forma di programmazione funzionale.
Alta montagna
Programmare in un linguaggio funzionale è difficile, soprattutto per chi ha iniziato con linguaggi imperativi, o forse serve una forma mentis particolare.
Tra i linguaggi funzionali iniziare a programmare in Lisp, o uno dei suoi dialetti, compreso il modernissimo Clojure, è un’esperienza terrorizzante ed esaltante insieme: un po’ come salire in alta montagna dove l’ossigeno scarseggia, ma il panorama in qualche modo ripaga. Trovarsi a dover usare un linguaggio che non ha variabili e non ha loop, lascia qualsiasi programmatore tradizionale con la sensazione di non potersi muovere. Credendoci se ne esce e si scopre che McCarthy, il creatore (qualcuno dice scopritore, intendendo che il Lisp sia una specie di archetipo universale della programmazione) ha creato qualcosa che che non si finisce mai di imparare eppure è incredibilmente semplice.
Di peso da Wikipedia
(cercando Paul Graham, uno degli evangelisti del Lisp).
Graham ha ipotizzato un fittizio linguaggio di programmazione, Blub, che si collocherebbe “esattamente a metà nel continuum dell’astrazione”. Ha usato tale linguaggio per illustrare il confronto tra il potere espressivo di differenti linguaggi di programmazione, al di là della Turing equivalenza, in particolare per mostrare la difficoltà nel paragonare un linguaggio che uno conosce con uno che non conosce.
Graham considera un ipotetico programmatore Blub. Quando questi guarda in basso nel “continuum della potenza”, considera i linguaggi inferiori come meno potenti, in quanto mancano di caratteristiche ai quali un programmatore Blub è abituato. Ma quando guarda verso l’alto, è incapace di rendersi conto che sta guardando verso l’alto: egli semplicemente vede degli strani linguaggi, con delle caratteristiche aggiuntive apparentemente non necessarie, che assume essere equivalenti in potere. Quando Graham considera il punto di vista di un programmatore che usa un linguaggio più in alto rispetto a Blub, invece, questi realizza le caratteristiche mancanti a Blub rispetto ad un linguaggio superiore.
Graham descrive questo fenomeno come il “paradosso di Blub” (Blub paradox) e conclude che “per induzione, gli unici programmatori in grado di vedere tutte le differenze di potenza tra i vari linguaggi di programmazione sono quelli che capiscono il linguaggio più potente”
Prossimo articolo della serie
The joy of programming. Le features dell’espressività
- oggi viene chiamato programmazione imperativa, ma solo per distinguerlo dalla programmazione funzionale. ↩