La versione audio non include la parte "Appendice: La potenza nei linguaggi" poiché contiene molte righe di codice utilizzate come esempi che non riescono ad essere riportate tramite la lettura a voce.
"Eravamo alla ricerca di programmatori C++. Riuscimmo nel compito di trascinare molti di loro a metà strada verso Lisp."
- Guy Steele, coautore della specifica di Java
Nell’industria del software è in corso una battaglia tra gli accademici dalla testa a punta e un’altra forza altrettanto formidabile: i capi dai capelli a punta. Tutti ricordano chi sia il capo dai capelli a punta, dico bene? Credo che la maggior parte del mondo tecnologico non solo riconosca il personaggio del fumetto, ma riesca persino a identificare una persona nella propria azienda simile a quella che lo ha ispirato.
Il capo dai capelli a punta combina miracolosamente, nella stessa persona, due qualità piuttosto comuni, ma che si vedono raramente assieme: (a) non sa nulla riguardo alla tecnologia, ma (b) ha opinioni nettissime su di essa.
Immaginate di dover scrivere un nuovo software. Il capo dai capelli a punta non ha idea di come funzionerà tale software, e non saprebbe distinguere un linguaggio dall’altro; eppure sa esattamente quale linguaggio dovreste usare allo scopo. Sì, proprio così: crede fermamente che dovreste usare Java.
Perché lo crede? Diamo un’occhiata all’interno del cervello del capo dai capelli a punta. Il suo ragionamento è pressappoco il seguente. Java è uno standard. Deve essere così, perché le notizie parlano continuamente di Java. Poiché è uno standard, scegliendolo non mi caccerò nei guai. Inoltre in giro ci saranno sempre un sacco di programmatori Java, così, se i programmatori che lavorano per me decidono di andar via, come misteriosamente accade spesso, posso rimpiazzarli con facilità.
Bisogna ammettere che non suona così irragionevole, eppure è un ragionamento basato su un assunto non palesato, e guarda caso tale assunto è falso. Il capo dai capelli a punta crede che tutti i linguaggi di programmazione siano grossomodo equivalenti. Se ciò fosse vero, avrebbe centrato il bersaglio. Se tutti i linguaggi si equivalgono, allora ben venga che si usi il linguaggio usato da tutti gli altri.
I linguaggi, però, non sono tutti uguali, e credo di poterlo provare senza neppure tirare in ballo le loro differenze. Se nel 1992 avessi chiesto al capo dai capelli a punta quale linguaggio scegliere per scrivere un software, avrebbe risposto senza alcuna esitazione - esattamente come fa oggi – che il software dovrebbe essere scritto in C++. Ma se tutti i linguaggi sono uguali, perché l’opinione del capo dai capelli a punta nel frattempo è cambiata? E poi, perché i progettisti di Java si sarebbero dovuti scomodare per creare un nuovo linguaggio?
Suppongo che se crei un nuovo linguaggio, è perché pensi possa essere migliore di quelli che la gente usa già. Almeno da certi punti di vista. Tanto che Goslin chiarisce, già nella prima pubblicazione su Java, che Java è stato progettato per superare certi problemi del C++. Ecco, dunque: i linguaggi non sono tutti uguali. Se seguite il sentiero che porta a Java attraverso il cervello del capo dai capelli a punta, e poi fate ritorno passando dalla storia di Java fino alle sue origini, vi ritroverete con un’idea che contraddice l’assunto col quale eravate partiti.
Allora chi ha ragione? James Gosling o il capo dai capelli a punta? Ovviamente ha ragione Gosling. Per certi problemi, alcuni linguaggi sono migliori di altri. E ciò solleva alcune domande interessanti. Java è stato progettato per essere migliore, almeno per certi problemi, di C++. Ma per quali problemi? Quand’è che Java è migliore? E quando lo è, piuttosto, C++? E poi, esistono casi in cui altri linguaggi sono migliori sia dell’uno che dell’altro?
Una volta presa in considerazione la questione, ci si accorge di aver scoperchiato un bel vespaio. Se il capo dai capelli a punta dovesse pensare al problema in tutta la sua complessità, gli esploderebbe il cervello. Finché considera tutti i linguaggi equivalenti, tutto ciò che deve fare è scegliere quello che lì per lì sembra essere più in voga, e dal momento che è una questione più di moda che di tecnologia, probabilmente anche lui riuscirà a ottenere la risposta giusta. Ma se i linguaggi hanno delle differenze, improvvisamente deve risolvere due equazioni simultanee, cercando di trovare un equilibrio ottimale tra due cose di cui non sa nulla: l'idoneità relativa di ognuno dei venti e più linguaggi utilizzabili per il problema che deve risolvere, e le probabilità di trovare programmatori, librerie, ecc. per ciascuno di essi. Se questo è ciò che c'è dall'altra parte della soglia, non sorprende che il capo dai capelli a punta non voglia attraversarla.
Lo svantaggio di credere che tutti i linguaggi siano equivalenti è quello di credere a una falsità. Ma il vantaggio è che tale convinzione semplifica di tanto la vita. Credo sia questo a rendere tale idea così diffusa: è un’idea comoda.
Siamo portati a credere che Java debba essere un linguaggio piuttosto buono; dopotutto è il nuovo, fantastico linguaggio di programmazione. Ma lo è davvero? Se da lontano guardi il mondo dei linguaggi di programmazione, in effetti sembra che Java sia l'ultima novità. (Dalla distanza, tutto ciò che puoi vedere è il grande e sfolgorante cartellone pubblicitario pagato da Sun.) Ma se guardi questo mondo più da vicino, scopri che esistono diversi gradi di entusiasmo per i linguaggi. All'interno della sottocultura hacker, c'è un altro linguaggio, chiamato Perl, che è considerato molto più interessante di Java. Slashdot, ad esempio, è generato da Perl. Non vedresti mai i ragazzi di Slashdot usare le Java Server Pages. Ma c'è un altro linguaggio più recente, chiamato Python, i cui utilizzatori tendono a disprezzare Perl, e altri nuovi linguaggi aspettano dietro le quinte di fare il loro ingresso.
Se si guarda a questi linguaggi in ordine, Java, Perl, Python, emerge uno schema interessante. Quantomeno lo si nota se si è programmatori Lisp. Ognuno di essi è progressivamente più simile a Lisp. Python copia persino certe caratteristiche di Lisp che molti utenti considerano errori. Potresti tradurre in Python dei semplici programmi scritti in Lisp riga per riga. Siamo nel 2002, e i linguaggi di programmazione hanno quasi raggiunto il 1958.
Al passo con la matematica
Sto alludendo al fatto che Lisp è stato scoperto per la prima volta da John McCarthy nel 1958, eppure solo ora i linguaggi di programmazione più popolari stanno recuperando il ritardo con le idee che McCarthy ha sviluppato allora.
Com’è possibile? L’informatica non è famosa per la sua evoluzione continua? Pensateci: nel 1958 i computer erano colossi delle dimensioni di un frigorifero, con la potenza di elaborazione di un orologio da polso. Come può una tecnologia così vecchia essere ancora interessante, e persino superiore agli ultimi sviluppi?
Il fatto è che Lisp, in origine, non è stato progettato per essere un linguaggio di programmazione, almeno non nel senso che intendiamo oggi. Ciò che intendiamo per linguaggio di programmazione è qualcosa che usiamo per dire a un computer cosa fare. In realtà, McCarthy intendeva davvero sviluppare un linguaggio di programmazione nel senso più comune, ma il Lisp con cui ci ritroviamo oggi è basato su qualcosa di diverso: un esercizio teorico che aveva come scopo quello di definire un'alternativa migliore alla macchina di Turing. Come disse in seguito McCarthy,
Un modo alternativo per dimostrare che Lisp fosse più espressivo delle macchine di Turing, era di scrivere una funzione Lisp universale e mostrare come il risultato fosse più breve e comprensibile della descrizione di una macchina di Turing universale. Questa funzione è nota come eval…, ed è usata per calcolare il valore di un'espressione Lisp. La scrittura di eval richiedeva l'invenzione di una notazione che rappresentasse le funzioni Lisp come dati di Lisp stesso. Tale notazione fu ideata per gli scopi dell'articolo. Non avrei mai pensato che una notazione del genere sarebbe stata utilizzata, nel concreto, per esprimere dei programmi in Lisp.
Quello che accadde dopo fu che, verso la fine del 1958, Steve Russell, uno degli studenti di McCarthy, guardò la definizione di eval e si rese conto che se l'avesse tradotta in linguaggio macchina, il risultato sarebbe stato un interprete Lisp.
Fu una grande sorpresa. Questo è quello che McCarthy ebbe a dire più avanti, durante un'intervista:
Steve Russell mi disse, Che ne dici se implemento davvero questa eval…? E io ho risposto, Oh, oh, stai confondendo la teoria con la pratica, la funzione eval è utile per spiegare un concetto, non per essere computata nella pratica. Ma lui è andato avanti lo stesso. In pratica ha tradotto la eval del mio articolo in codice macchina per l’IBM 704, correggendo i bug, e poi dichiarando che il risultato fosse un interprete Lisp, cosa che in effetti era davvero. A quel punto, Lisp ha preso essenzialmente la forma che ha oggi…
All'improvviso, nel giro di poche settimane credo, McCarthy ha visto il suo esercizio teorico trasformarsi in un vero e proprio linguaggio di programmazione, e pure più potente di quanto avesse previsto.
Quindi la spiegazione del perché questo linguaggio degli anni '50 non è obsoleto è che non si tratta di tecnologia, ma di matematica, e la matematica non invecchia. La cosa giusta a cui paragonare Lisp non è l'hardware degli anni '50, ma, ad esempio, l'algoritmo Quicksort, che è stato scoperto nel 1960 ed è ancora l'algoritmo di ordinamento generale più veloce in circolazione.
C'è un altro linguaggio che sopravvive dagli anni '50 fino a oggi: il Fortran. Rappresenta l'approccio opposto alla progettazione di un linguaggio. Lisp era un pezzo di teoria che inaspettatamente si è trasformato in un linguaggio di programmazione. Fortran è stato sviluppato intenzionalmente come linguaggio di programmazione, ma a un livello di astrazione che ora giudichiamo molto basso.
Il Fortran I, così come si presentava il linguaggio nel 1956, era un animale molto diverso dall'attuale Fortran. Fortran I era praticamente il linguaggio assembly con l’aggiunta della matematica. Per certi versi era meno potente dei linguaggi assembly più recenti; non c'erano subroutine, ad esempio: solo i salti condizionali. Il Fortran di oggi è probabilmente più simile al Lisp che al Fortran I.
Lisp e Fortran erano i tronchi di due alberi evolutivi separati, uno fondato sulla matematica, l’altro sull'architettura delle macchine. Da allora questi due alberi stanno convergendo. Lisp è nato potente, e nei successivi vent'anni è diventato veloce. I cosiddetti linguaggi mainstream sono nati veloci, e nei successivi quarant'anni sono diventati gradualmente più potenti, fino al punto che ormai i più avanzati sono abbastanza vicini al Lisp. Vicini, ma mancano ancora alcune cose...
Cosa rende Lisp così diverso
Quando è stato sviluppato per la prima volta, Lisp incarnava nove idee che a quel tempo erano una novità. Alcune di queste ora le diamo per scontate, altre esistono solo nei linguaggi più avanzati. Due sono ancora prerogativa unica di Lisp. Le nove idee sono, in ordine di adozione da parte degli altri linguaggi, le seguenti:
* L’esecuzione condizionale. Una condizionale è definita dal costrutto if-then-else. Oggi diamo questo costrutto per scontato, ma Fortran non lo possedeva. Disponeva soltanto di un salto condizionale basato sulle istruzioni della macchina.
* Le funzioni come tipo di dato. In Lisp, le funzioni sono un dato come lo sono gli interi e le stringhe. Hanno una rappresentazione letterale, possono essere registrate nelle variabili, passate come argomenti, e così via.
* La ricorsione. Lisp è stato il primo linguaggio di programmazione a supportarla.
* I tipi dinamici. In Lisp, tutte le variabili di fatto sono dei puntatori. Sono i valori ad avere un tipo, non le variabili. Assegnare o collegare dei valori alle variabili equivale a copiare dei puntatori, e non i dati a cui tali puntatori si riferiscono.
* La gestione automatica della memoria (Garbage-collection).
* Programmi composti da espressioni. I programmi scritti in Lisp sono alberi di espressioni annidate, ognuna delle quali ritorna un valore. Ciò è diverso da quel che accade nei linguaggi più diffusi, che distinguono tra espressione e istruzioni.In Fortran I, avere questa distinzione era naturale, perché le istruzioni non potevano essere annidate. E così, nonostante le espressioni fossero utili affinché la matematica funzionasse, non aveva senso fare in modo che gli altri costrutti restituissero un valore; non poteva esserci nulla ad aspettarlo.Questa limitazione scomparve con l'arrivo dei linguaggi strutturati che facevano uso dei blocchi, ma ormai era troppo tardi. La distinzione tra espressioni e istruzioni era radicata. Si è diffusa da Fortran ad Algol, e poi ai loro discendenti.
* Un tipo simbolico. I simboli sono effettivamente puntatori a stringhe memorizzate in una tabella. Quindi si può testare l'uguaglianza di due stringhe confrontandone i puntatori, invece di confrontare ogni carattere che le compone.
* Una notazione per il codice che fa uso di alberi di simboli e costanti.
* L'intero linguaggio sempre disponibile. Non esiste una vera distinzione tra il tempo di lettura del sorgente, il tempo di compilazione e il tempo di esecuzione. È possibile compilare o eseguire codice durante la fase di lettura, leggere o eseguire codice durante la compilazione e leggere o compilare codice in fase di esecuzione.L'esecuzione del codice in fase di lettura consente agli utenti di riprogrammare la sintassi di Lisp; l'esecuzione del codice in fase di compilazione è la base delle macro; la compilazione in fase di esecuzione è la base dell'uso di Lisp come linguaggio di estensione in programmi come Emacs; e la lettura in fase di esecuzione consente ai programmi di comunicare utilizzando le s-expression, un'idea recentemente reinventata col nome di XML.
Quando Lisp apparve per la prima volta, queste idee erano molto lontane dalla normale pratica della programmazione, dettata in gran parte dai limiti dall'hardware disponibile alla fine degli anni '50. Nel corso del tempo, il linguaggio più utilizzato in un dato periodo è stato incarnato via via da una successione di linguaggi popolari sempre più simili al Lisp. Le idee dalla 1 alla 5 sono ormai diffuse. La numero 6 sta iniziando ad apparire nei linguaggi più popolari. Python implementa in qualche forma l'idea numero 7, anche se non ha alcuna sintassi esplicita a supporto.
L’idea numero 8 potrebbe essere la più interessante del lotto. Le idee 8 e 9 sono diventate parte di Lisp solo per caso, perché Steve Russell ha implementato qualcosa che McCarthy non aveva mai pensato di implementare. Eppure queste idee risultano essere responsabili sia dello strano aspetto di Lisp che delle sue caratteristiche più distintive. Lisp ha un aspetto strano non tanto perché ha una sintassi strana, ma perché non ha alcuna sintassi; i programmi sono espressi direttamente negli stessi alberi di parsing che vengono usati dietro le quinte, mentre la sintassi degli altri linguaggi viene prima processata. Gli alberi usati da Lisp sono costituiti usando direttamente le liste, che sono strutture dati Lisp native.
Esprimere il linguaggio nelle proprie strutture dati può renderlo molto potente. Le idee 8 e 9, messe assieme, significano che puoi scrivere programmi che a loro volta scrivono programmi. Può sembrare un'idea bizzarra, ma è una cosa usata quotidianamente in Lisp. Il modo più comune per farlo è attraverso una funzionalità chiamata macro.
Il termine "macro" non significa, in Lisp, ciò che significa negli altri linguaggi. Una macro Lisp può essere qualsiasi cosa, da una semplice abbreviazione a un compilatore per un nuovo linguaggio. Chi volesse veramente capire Lisp, o semplicemente espandere i propri orizzonti di programmazione, dovrebbe imparare di più sulle macro.
Le macro (per come sono in Lisp) sono ancora, per quanto ne so, una prerogativa esclusiva di Lisp. Ciò è in parte dovuto al fatto che per avere le macro, probabilmente devi rendere il tuo linguaggio strano come Lisp. O magari è anche dovuto al fatto che se potenzi un linguaggio in tal modo, non puoi affermare di aver inventato un nuovo linguaggio, ma al più un nuovo dialetto di Lisp.
A volte dico questa cosa per scherzo, ma è abbastanza vera. Se definisci un linguaggio che ha car, cdr, cons, quote, cond, atom, eq e una sintassi per esprimere le funzioni come liste, da questa base puoi ricavare l’intero linguaggio Lisp. È questa la qualità distintiva di Lisp, e McCarthy ha dato al Lisp la forma che ha affinché avesse tale qualità.
Quando i linguaggi contano
Supponiamo quindi che Lisp rappresenti una sorta di limite a cui i linguaggi tradizionali si stanno avvicinando asintoticamente. Ciò implica necessariamente che sia l’ideale per scrivere software? Quanto perdi usando un linguaggio meno potente? Non è più saggio, a volte, stare lontani dal limite massimo dell’innovazione? E la popolarità non giustifica, in qualche misura, se stessa? Forse ha ragione il capo dai capelli a punta quando sceglie un linguaggio per il quale può facilmente assumere programmatori?
Esistono, ovviamente, progetti in cui la scelta del linguaggio di programmazione non ha molta importanza. Di norma, più l'applicazione è impegnativa, maggiore è l'effetto leva ottenuto dall'uso di un linguaggio potente. Ma molti progetti non sono così impegnativi. La maggior parte della programmazione probabilmente consiste nello scrivere piccoli programmi che incollano tra di loro diversi pezzi. Per i piccoli programmi di questo tipo si può usare qualsiasi linguaggio con cui si ha già familiarità, a condizione che possieda buone librerie per il problema che si vuole risolvere. Se si ha solo bisogno di inviare dati da un'app di Windows a un'altra, va bene usare Visual Basic.
Si possono scrivere piccoli programmi anche usando Lisp (io lo uso come calcolatrice da tavolo), ma la vittoria più grande, per i linguaggi come Lisp, sta dall'altra parte dello spettro, laddove è necessario scrivere programmi sofisticati per risolvere problemi difficili, mentre i concorrenti ti stanno col fiato sul collo. Un buon esempio è il programma di ricerca delle tariffe aeree che ITA Software concede in licenza a Orbitz. Questi ragazzi sono entrati in un mercato già dominato da due grosse società, Travelocity ed Expedia, e sembrano averle umiliate tecnologicamente.
Il nucleo dell'applicazione di ITA è un programma in Common Lisp di 200.000 righe che esegue le ricerche su uno spazio di possibilità più grande di diversi ordini di grandezza rispetto ai loro concorrenti, che a quanto pare utilizzano ancora tecniche di programmazione dell'era dei mainframe (Sebbene ITA, in un certo senso, utilizzi un linguaggio di programmazione dell'era dei mainframe). Non ho mai visto il codice di ITA, ma secondo uno dei loro migliori hacker usano molte macro. Non mi sorprende.
Forze centripete
Non dico che non ci sia alcun costo dovuto all'utilizzo di tecnologie inusuali. Le preoccupazioni del capo dai capelli a punta non sono del tutto infondate. Tuttavia, poiché non comprende i rischi, tende a ingigantirli.
Mi vengono in mente almeno tre problemi che potrebbero scaturire dall'utilizzo di linguaggi meno comuni. I programmi potrebbero non interagire bene con i programmi scritti in altri linguaggi, ci sono meno librerie a disposizione e assumere programmatori potrebbe non essere così facile.
Quanto conta davvero ognuno di questi limiti? L'importanza del primo varia a seconda che tu abbia il controllo dell'intero sistema. Se stai scrivendo un software che deve girare sulla macchina di un utente remoto, su un sistema operativo chiuso e difettoso (non faccio nomi), potrebbero esserci dei vantaggi nello scrivere la tua applicazione nello stesso linguaggio del sistema operativo. Viceversa, se hai il controllo dell'intero sistema e hai il codice sorgente di tutte le parti, come presumibilmente ha ITA, puoi usare il linguaggio che ti pare. In caso di incompatibilità puoi risolvere il problema in autonomia.
Nelle applicazioni lato server, puoi concederti di usare le tecnologie più avanzate, e penso che questa sia la causa principale di ciò che Jonathan Erickson chiama il "rinascimento dei linguaggi di programmazione". È questo il motivo per cui sentiamo parlare di nuovi linguaggi come Perl e Python. Non ne parlano certo quelli che scrivono applicazioni per Windows; ne parlano quelli che scrivono applicazioni lato server. E man mano che il software passa dal desktop ai server (un futuro a cui anche Microsoft sembra rassegnarsi), ci saranno sempre meno spinte verso l’utilizzo di tecnologie mediocri.
Per quanto riguarda le librerie, la loro importanza dipende dall'applicazione da sviluppare. Per problemi meno impegnativi, la disponibilità di librerie appropriate può superare i vantaggi dati dalla potenza intrinseca del linguaggio. Dov'è il punto di pareggio? Difficile dirlo con esattezza, ma ovunque si trovi, non è lontano da qualsiasi cosa si possa definire un'applicazione vera e propria. Se un'azienda che basa il suo business sul software sta scrivendo un'applicazione che diventerà uno dei prodotti di punta, probabilmente coinvolgerà diversi programmatori esperti, e lo sviluppo richiederà almeno sei mesi. In un progetto di tali dimensioni, si può supporre che l’utilizzo di linguaggi potenti superi la comodità di avere librerie già pronte.
La terza preoccupazione del capo dai capelli a punta è la difficoltà di assumere programmatori. Penso sia illusoria. Di quanti programmatori esperti avrà bisogno, dopotutto? Ormai sappiamo tutti che il software è sviluppato al meglio da team più piccoli di dieci persone. Non dovrebbe essere così difficile trovare così pochi programmatori, qualunque sia il linguaggio usato. Se un'azienda non riesce a trovare dieci programmatori Lisp esperti, probabilmente è stata fondata nella città sbagliata per lo sviluppo di software.
La scelta di un linguaggio potente potrebbe persino ridurre le dimensioni del team di cui si ha bisogno, perché (a) se si usa un linguaggio potente probabilmente si potrà fare di più con meno programmatori, e (b) i programmatori che lavorano in linguaggi avanzati probabilmente si riveleranno più capaci.
Non nego che ci sia molta pressione verso l’utilizzo di quelle che sono percepite come tecnologie "standard". A Viaweb (ora Yahoo Store) per aver scelto Lisp abbiamo fatto sollevare diverse sopracciglia, tra investitori e potenziali acquirenti. Ma le sopracciglia si sono sollevate anche per il fatto di usare normali computer PC come server, invece di server "di qualità industriale", come quelli della Sun. E ancora per aver utilizzato un'oscura variante di Unix open source chiamata FreeBSD, invece di un vero sistema operativo commerciale come Windows NT, e per aver ignorato un presunto standard commerciale chiamato SET, che ora nessuno più ricorda. E così via.
Non puoi lasciare che i dirigenti prendano decisioni tecniche per te. L’aver utilizzato Lisp ha preoccupato dei potenziali acquirenti? Sì, un po'. Ma se non avessimo usato Lisp, non saremmo stati in grado di scrivere il software che li ha spinti a comprarci. Quella che sembrava loro un'anomalia era in realtà una conseguenza di causa ed effetto.
Se crei una startup, non progettare il tuo prodotto per soddisfare gli investitori o i potenziali acquirenti. Progetta il tuo prodotto per soddisfare gli utenti. Se vinci la fiducia degli utenti, tutto il resto seguirà. E se non lo fai, a nessuno importerà quanto siano state ortodosse e rassicuranti le tue scelte tecnologiche.
Il costo di stare nella media
In che misura perdi in produttività usando un linguaggio meno potente? A proposito della questione, sono stati pubblicati dei dati.
La misura più pratica della potenza di un linguaggio è probabilmente la dimensione del codice. Lo scopo dei linguaggi di alto livello è quello di fornire astrazioni di più alto livello - mattoni più grandi, per così dire, quindi non te ne servono tanti per costruire un muro di una data dimensione. Quindi più potente è il linguaggio, più corto è il programma (non solo in caratteri, ovviamente, ma in elementi distinti).
In che modo un linguaggio più potente consente di scrivere programmi più brevi? Una tecnica utilizzabile, se il linguaggio lo consente, è quella della programmazione bottom-up. Invece di scrivere semplicemente una applicazione nel linguaggio base, si costruisce sopra di esso un linguaggio specializzato per scrivere programmi di quel particolare tipo, quindi si scrive il programma in questo nuovo linguaggio. La somma della lunghezza del codice che è stato scritto per implementare le astrazioni, e quella del codice della applicazione stessa, può essere molto più breve rispetto alla applicazione equivalente scritta in maniera più diretta nel linguaggio base. In effetti è così che funziona la maggior parte degli algoritmi di compressione. Un programma scritto con la tecnica bottom-up è anche più facile da modificare, perché in molti casi la parte che implementa le astrazioni di base non dovrà essere modificata.
La dimensione del codice è importante perché il tempo necessario per scrivere un programma dipende principalmente dalla sua lunghezza. Se un programma è tre volte più lungo in un linguaggio diverso, occorre tre volte il tempo per scriverlo. Questo ostacolo non è aggirabile assumendo più persone, perché oltre una certa dimensione i nuovi assunti fanno perdere tempo invece che farlo guadagnare. Fred Brooks ha descritto questo fenomeno nel suo famoso libro The Mythical Man-Month, e tutto ciò che ho visto nella mia esperienza tende a confermare ciò che Brooks ha scritto.
Quindi gli stessi programmi scritti in Lisp sarebbero più brevi? La maggior parte dei numeri che ho visto dicono che i programmi scritti in Lisp, rispetto a quelli scritti in C, sono circa 7-10 volte più brevi. Ma un recente articolo su ITA, nella rivista New Architect, diceva che "una riga di Lisp può sostituire 20 righe di C" e poiché questo articolo era pieno di citazioni del presidente di ITA, presumo che abbiano ricevuto questo numero direttamente da ITA. Se è così, allora possiamo riporre in esso un po' di fiducia; Il software di ITA è scritto anche in C e C++, oltre che in Lisp, quindi parlano per esperienza.
La mia ipotesi è che questi moltiplicatori non siano nemmeno costanti. Penso che aumentino quando affronti problemi più difficili e anche quando hai programmatori più capaci. Un hacker davvero bravo può ottenere di più da strumenti migliori.
Come esempio specifico, in ogni caso, se dovessi competere con ITA e scegliessi di scrivere il tuo software in C, loro sarebbero in grado di sviluppare software venti volte più velocemente di te. Se tu trascorressi un anno a sviluppare una nuova funzionalità, sarebbero in grado di duplicarla in meno di tre settimane. Al contrario se usassero solo tre mesi per sviluppare qualcosa di nuovo, a te servirebbero cinque anni prima di avere una funzionalità equivalente.
E sai cosa credo? Che questo sia lo scenario migliore. Quando ci si sofferma sui rapporti di dimensioni del codice, si presume implicitamente che si possa effettivamente scrivere il programma anche nel linguaggio meno potente. Ma in realtà ci sono limiti a ciò che i programmatori possono fare. Se stai cercando di risolvere un problema difficile con un linguaggio di livello troppo basso, ti ritrovi a un punto in cui ci sono troppi dettagli da tenere in mente allo stesso tempo.
Quindi, quando dico che il concorrente immaginario di ITA impiegherebbe cinque anni per duplicare qualcosa che ITA potrebbe scrivere in Lisp in tre mesi, intendo dire cinque anni nel caso in cui nulla andasse storto. In effetti, per come funzionano le cose nella maggior parte delle aziende, è probabile che un progetto di sviluppo che richiederebbe cinque anni non verrebbe mai completato.
Ammetto che questo è un caso estremo. I programmatori di ITA sembrano essere insolitamente capaci, e il C è un linguaggio piuttosto di basso livello. Ma in un mercato competitivo, basterebbe anche un differenziale di due o tre a uno per garantire di essere sempre indietro.
La ricetta
Questo che ho dipinto è uno scenario a cui il capo dai capelli a punta non vuole nemmeno pensare. E quindi la maggior parte dei capi non lo fa. Perché, sai, alla fin fine, al capo dai capelli a punta non importa che la sua azienda venga presa a calci nel sedere, purché nessuno possa dimostrare che è colpa sua. Il piano più sicuro per garantire la sua incolumità è di rimanere assieme al resto della mandria.
All'interno delle grandi organizzazioni, la frase usata per descrivere questo approccio è “best practice” del settore. Il suo scopo è proteggere il capo dai capelli a punta dalle responsabilità: se sceglie qualcosa che è in linea con "la migliore pratica del settore" e l'azienda perde, non può essere biasimato. Non ha scelto lui, l'ha fatto l'intera industria al suo posto.
Credo che questo termine fosse originariamente usato per descrivere i metodi contabili. Ciò che significa, grosso modo, è di non fare niente di troppo strano. E in contabilità probabilmente è una buona idea. I termini "all'avanguardia" e "contabilità" non stanno bene insieme. Ma se si importa questo criterio nelle decisioni tecnologiche, si iniziano a ottenere risposte sbagliate.
La tecnologia dovrebbe essere all'avanguardia. Nei linguaggi di programmazione, come ha sottolineato Erann Gat, ciò che la "migliore pratica del settore" ti offre effettivamente non è il meglio, ma la mediocrità. Quando una certa decisione ti porta a sviluppare il software a una frazione del tempo impiegato dai concorrenti più aggressivi, "best practice" è un termine improprio.
Siamo giunti a due informazioni che reputo preziose: lo so per esperienza. Numero 1, i linguaggi di programmazione hanno una potenza variabile. Numero 2, la maggior parte dei manager ignora questo fatto deliberatamente. Insieme, questi due fatti sono letteralmente una ricetta per fare soldi. ITA è un esempio pratico di questa ricetta. Se vuoi vincere con un'azienda di software, affronta il problema più difficile che riesci a trovare, usa il linguaggio più potente che riesci a ottenere e attendi che i capi dai capelli a punta, alla guida delle aziende concorrenti, si accontentino della mediocrità.
Appendice: La potenza nei linguaggi
Per chiarire a cosa mi riferisco quando parlo della potenza relativa dei linguaggi di programmazione, è utile considerare il seguente problema: la scrittura di una funzione che genera degli accumulatori. Un accumulatore è una funzione che prende un numero n e ritorna una funzione che prende in ingresso un altro numero i, e a ogni invocazione ha come valore di ritorno n incrementato di i.
(Si noti che il valore di n viene incrementato ogni volta: la funzione non torna semplicemente la somma tra il valore originale di n e i. Un accumulatore accumula. NdT: la funzione originale ritorna dunque delle chiusure, il cui stato, in questo caso la variabile n, persiste durante le chiamate).
In Common Lisp ciò si scriverebbe così:
e in Perl 5,
Che ha più elementi del programma Lisp, perché in Perl bisogna estrarre i parametri manualmente.
In Smalltalk il codice è di poco più lungo che in Lisp
Perché nonostante in generale le variabili con scopo lessicale funzionino, non si può assegnare un valore a un parametro della funzione, dunque è necessario creare una nuova variabile s.
Anche in Javascript l’esempio è un po’ più lungo, perché Javascript distingue tra espressione e dichiarazione, così serve usare la keyword return in maniera esplicita:
(A dire la verità anche Perl conserva tale distinzione, ma la risolve nel modo consono a Perl: permettendo una omissione)
Se si traduce il codice in Lisp/Perl/Smalltalk/Javascript in Python, ci si accorge di certe limitazioni del linguaggio. Python non supporta appieno lo scopo lessicale delle variabili, e ciò rende necessario creare una struttura dati per memorizzare il valore di n. E nonostante Python abbia un tipo di dato per rappresentare le funzioni, manca di una sintassi per le funzioni letterali (a meno ché il corpo della funzione non sia composto da una sola espressione). Per tale ragione serve creare una funzione che abbia un nome e usarla come valore di ritorno. Si finisce con questo codice:
Gli utenti di Python potrebbero legittimamente chiedersi come mai non possono scrivere solo:
O anche semplicemente:
E scommetto che potranno, un giorno. (Ma se non volessero aspettare che Python si evolva del tutto verso Lisp, potrebbero semplicemente…)
Nei linguaggi a oggetti, in qualche modo è possibile simulare una chiusura (una funzione che si riferisce alle variabili definite in uno scopo chiuso) definendo una classe con un metodo e un attributo per ogni variabile nello scopo che si vuole racchiudere. Ciò fa fare al programmatore lo stesso sforzo di analisi che di solito viene svolto dal compilatore di un linguaggio col supporto per lo scopo lessicale, e un tale approccio non funziona nel caso di più funzioni che si riferiscono alla stessa variabile. Tuttavia, nel caso in esame tale approccio è sufficiente.
Gli esperti di Python sembrano concordi sul fatto che il modo migliore per risolvere il problema in Python sia scrivere il seguente codice.
O in alternativa:
Ho incluso queste versioni per non sentirmi dire dai promotori di Python che non ho ben capito il loro linguaggio, però questo codice sembra più complicato dalla prima versione proposta. Il concetto di base è uguale: la creazione di un meccanismo che permetta di memorizzare l’accumulatore; in questo caso è un attributo di un oggetto invece che il primo elemento di una lista. Inoltre l’utilizzo di questi nomi riservati, quali __call__, ha un po’ l’aria di essere un trucco.
Nella rivalità tra Perl e Python, l’argomento degli esperti di Python sembra essere che, tra i due, Python è un'alternativa più elegante, ma questo specifico caso mostra che la vera eleganza è la potenza: il programma in Perl è più semplice (ha meno elementi), nonostante la sintassi sia un po’ più sporca.
E gli altri linguaggi? Nei restanti di cui ho parlato in questo intervento – Fortran, C, C++, Java e Visual Basic – non è chiaro se il problema è effettivamente risolvibile. Ken Anderson dice che il codice che segue è il meglio che si riesca a fare in Java:
Ma non è all'altezza delle specifiche perché funziona solo per i numeri interi. Dopo molti scambi di email con gli esperti di Java, direi che scrivere una versione propriamente polimorfica che si comporti come gli esempi precedenti è qualcosa che sta tra l'imbarazzante e l'impossibile. Se qualcuno volesse scriverne una versione, sarei molto curioso di vederla, ma personalmente ho smesso di provarci.
Non è strettamente vero che non puoi risolvere questo problema in altri linguaggi, ovviamente. Il fatto che tutti questi linguaggi siano equivalenti a una macchina di Turing significa che, in senso stretto, puoi scrivere qualsiasi programma in ognuno di essi. Quindi come si risolve il problema? Nel caso limite, scrivendo un interprete Lisp nel linguaggio meno potente.
Sembra uno scherzo, ma succede così spesso a vari livelli nei grandi progetti di programmazione che c'è un nome per questo fenomeno, che poi è la decima regola di Greenspun:
Qualsiasi programma C o Fortran sufficientemente complicato contiene un'implementazione lenta, piena di bug, e non documentata di metà di Common Lisp.
Se provi a risolvere un problema difficile, la domanda non è se utilizzerai un linguaggio abbastanza potente, ma se (a) utilizzerai un linguaggio potente, (b) scriverai un interprete ad-hoc per uno o (c) diventerai tu stesso un compilatore umano per un linguaggio più potente. Tutto ciò sta già iniziando ad accadere nell'esempio in Python, dove in effetti stiamo simulando il codice che un compilatore genererebbe per implementare una variabile lessicale.
Questa pratica non è solo comune, ma istituzionalizzata. Ad esempio, nel mondo della programmazione a oggetti si sente parlare molto di "pattern". Mi chiedo se questi pattern a volte non siano la prova del caso (c): il compilatore umano al lavoro. Quando vedo pattern nei miei programmi, li considero una avvisaglia di problemi. La forma di un programma dovrebbe riflettere solo il problema che deve risolvere. Qualsiasi altra regolarità nel codice è un segno, almeno per me, che sto usando astrazioni che non sono abbastanza potenti, e spesso il problema è che sto generando a mano le espansioni di qualche macro che avrei dovuto scrivere.
Note
* La CPU IBM 704 aveva all'incirca le dimensioni di un frigorifero, ma era molto più pesante. La CPU pesava 3150 libbre e i 4K di RAM erano in una scatola separata, del peso di ulteriori 4000 libbre. Il Sub-Zero 690, uno dei più grandi frigoriferi domestici, pesa 656 libbre.
* Steve Russell ha anche scritto il primo gioco per computer (digitale), Spacewar, nel 1962.
* Se vuoi indurre un capo dai capelli a punta a lasciarti scrivere software in Lisp, potresti provare a dirgli che è XML.
* Ecco il generatore di accumulatori in altri dialetti di Lisp: Scheme: (define (foo n) (lambda (i) (set! n (+ n i)) n))Goo: (df foo (n) (op incf n _)))Arc: (def foo (n) [++ n _])
* La triste storia di Erann Gat sulle "migliori pratiche del settore" al JPL mi ha ispirato ad affrontare la questione di questa frase generalmente applicata erroneamente.
* Peter Norvig ha affermato che 16 dei 23 pattern in Design Patterns erano "invisibili o più semplici" in Lisp.
* Grazie alle molte persone che hanno risposto alle mie domande su vari linguaggi e/o hanno letto bozze di questo articolo, tra cui Ken Anderson, Trevor Blackwell, Erann Gat, Dan Giffin, Sarah Harlin, Jeremy Hylton, Robert Morris, Peter Norvig, Guy Steele e Anton van Straten. Non hanno alcuna colpa per le opinioni espresse.
Seguito:
In tanti hanno risposto a questo discorso, quindi ho creato una pagina aggiuntiva per affrontare i problemi che hanno sollevato: Re: Revenge of the Nerds.
Ha anche avviato un'ampia e spesso utile discussione sulla mailing list LL1. Si veda in particolare la mail di Anton van Straaten sulla compressione semantica.
Alcune email sulla mailing list LL1 mi hanno portato a cercare di approfondire l'argomento della potenza dei linguaggi Succinctness is Power.
Un insieme più ampio di implementazioni canoniche del benchmark del generatore di accumulatori è stato raccolto in una pagina specifica.
This is a public episode. If you would like to discuss this with other subscribers or get access to bonus episodes, visit paulgrahamita.substack.com