giovedì 25 settembre 2008

Reinventare la ruota

Come ho scritto nel post precedente, l'OOP non è la panacea per tutti i mali del mondo della programmazione, anzi in alcuni casi gli effetti collaterali di un presunto beneficio sono, sul lungo periodo, devastanti!
È il caso questo del “riutilizzo del software”. Sembra una cosa bella, innocua, decisamente utile, ma è il portone da cui parte la strada verso l'ottundimento della mente creativa dei programmatori.
Mi spiego. Qualche tempo fa mi sono trovato nella necessità di leggere un file XML da programma per estrarne alcuni valori. Era un piccolo file, al massimo una ventina di righe di XML. Allora ho scritto un piccolo parser che identificasse ed estraesse i valori che mi interessavano. In tutto, il parser, consisteva in un centinaio di righe di codice C++ e “pesava” 5 o 6 KB. A questo punto la domanda che è la chiave di questo post. Mi sono sentito chiedere: “Perché non hai usato una libreria già consolidata per leggere il tuo file XML? In Internet puoi trovarne a centinaia”. È vero in Internet posso trovare tutte le librerie e frameworks per leggere un file XML. Istintivamente ho risposto che mi sembrava stupido utilizzare un framework (come ad esempio XERCES) di 30 MB per leggere un piccolo file XML di 20 righe.
Purtroppo questa è la realtà. Il riutilizzo del software porta a questo. Una volta qualcuno ha scritto un qualcosa per risolvere un certo problema, e da allora tutti si è obbligati ad utilizzare quel qualcosa per risolvere anche i nostri di problemi. Il motto del riutilizzo del software è: “è stupido reinventare la ruota tutte le volte”.
Questa sembra essere anche la filosofia di molti prodotti commerciali come ad esempio il C++ Builder della Borland (ok, d'accordo, C++ Builder non è più Borland, ma mi piace ricordarlo come tale). In C++ Builder, esistevano (ed esistono tutt'ora) “componenti” per ogni piccola attività. Volevi aprire un socket? Niente di più semplice. Prendevi il componentino socket e lo trascinavi sulla tua form. Volevi creare un thread? Stessa procedura. Vuoi leggere un file XML? E che problema c'è, il Builder ha il componente giusto per te. In C++ Builder esiste un componente per ogni cosa si voglia fare e senza scrivere una linea di codice. Questo non è più programmare! È solo giocare con dei mattoncini LEGO per costruire il nostro giocattolino. Può sembrare assurdo, ma nella grossa ditta in cui sto prestando servizio come consulente, ho conosciuto delle persone che sono state assunte come programmatori, e che non sono in grado neppure di scrivere le 6 righe di codice in C++ per aprire, leggere e bufferizzare un file testuale. Questo perché lavorano con un ambiente di sviluppo che non gli mette a disposizione il componente adatto per farlo. Come vi ho detto sembra assurdo, ma vi giuro che è la pura verità.
Per fortuna esistono ancora dei programmatori che non hanno paura di sporcarsi le mani a scrivere codice, cavalieri della programmazione che combattono tutti i giorni contro l'ottusità di chi preferisce giocare con il LEGO (non che a giocare con i mattoncini LEGO ci sia qualcosa di male, io lo adoro, ma la programmazione è un'altra cosa).
Per questo tutta la mia stima va a quei programmatori di Google che per scrivere Chrome hanno deciso di reinventare la ruota. Nonostante esistessero letteralmente centinaia di interpreti java-script già esistenti, loro hanno deciso di scriversene uno tutto loro. E, guarda caso, questo funziona meglio ed è decisamente più veloce (dalle due alle tre volte) rispetto a tutti gli interpreti già pronti a essere riutilizzati. Forse reinventare la ruota tutte le volte non è poi così stupido. Forse rimettersi a fare i programmatori ha i suoi vantaggi e risparmiare tempo a tutti i costi riutilizzando software già scritto non paga poi così tanto. Cosa sarebbe l'arte se tutti gli artisti decidessero di non reinventare la ruota tutte le volte? Ovviamente ognuno è libero di rimanere della propria idea e continuare a considerare l'OOP come il vero ed unico passo avanti nell'arte della programmazione. Quindi ognuno è libero di trarre le proprie conclusioni da questo post. L'importante è non utilizzare un componente già pronto per farlo.

Luca Ciciriello

sabato 20 settembre 2008

“Parla, SHRDLU, parla perché possa capirti...”

Sono sicuro che molti di voi avranno riconosciuto in queste parole il titolo di uno dei capitoli del libro di D. R. Hofstadter “Gödel, Escher, Bach: un'eterna ghirlanda brillante”. Il perché io abbia scelto questo titolo lo vedremo fra breve.
La gente è proprio strana. Da quando è stato proposto il paradigma ad oggetti fino ad oggi, un programmatore è praticamente obbligato a scrivere programmi OO.
L'OOP risolve alcuni dei problemi che hanno afflitto il mondo della programmazione come la modularità e la riusabilità del software, ma non è la panacea per tutti i mali del mondo. La programmazione procedurale è tutt'altro che defunta, soprattutto per quanto riguarda il multithreading, e che dire poi della programmazione funzionale? Ed è proprio di programmazione funzionale che parlerò in questo post. Sì, perché se la mia lingua madre è il C++, la mia seconda lingua è sicuramente il LISP e più precisamente quello che è diventato lo standard de-facto del LISP ovvero il Common LISP (CLISP, nell'implementazione GNU che uso io su MacOS X, abbinato a EMACS).
Il LISP è un linguaggio funzionale, cioè tratta simboli invece che valori utilizzando l'eleganza della notazione polacca inversa. Infatti LISP sta per LISt Processing. Inventato nel 1958 da John McCarthy al MIT ed implementato da Steve Russell (famoso per aver creato SpaceWar il primo videogame della storia) su un IBM 704, vanta il primato di essere il più antico linguaggio di programmazione ancora pienamente in uso ai giorni nostri. È il linguaggio usato di preferenza nel campo della ricerca sull'AI e nello studio del linguaggio naturale applicato ai calcolatori. Proprio nel campo della ricerca sul linguaggio naturale troviamo l'ormai leggendario programma SHRDLU scritto dall'altrettanto leggendario maestro Terry Allen Winograd nel 1972 alla Stanford University. A mio avviso (e non solo mio), SHRDLU è uno dei programmi più geniali ed illuminanti nell'intera storia della programmazione. Scritto in “Micro Planner”, un'implementazione LISP del linguaggio PLANNER, racchiude il seme di molte idee riprese poi in seguito in altri sistemi anche commerciali come sistemi operativi e compilatori. Il kernel di SHRDLU (contenuto nel file della versione originale plnr.lisp) rientra nella categoria dei “theorem proover”. SHRDLU è in grado di ricevere i suoi input nel normale inglese parlato, eseguire le azioni che gli vengono chieste e produrre i suoi output sempre in inglese. Quello che fa SHRDLU (qui sto semplificando molto un sistema estremamente più complesso) è esaminare la frase inglese fornita in input attraverso un sistema sintattico-semantico (eta oin) e trasformare questo input in sentenze logiche (teoremi) analizzabili dal theorem proover. Anche se la “conoscenza dell'universo” di SHRDLU è limitata al cosiddetto “mondo dei blocchi”, questo programma rappresenta un passo importantissimo nella reale comprensione di una frase in linguaggio naturale da parte di un sistema informatico. Sicuramente il successo di SHRDLU risiede in gran parte nel fatto di essere scritto in LISP. L'enorme flessibilità di questo linguaggio dà a chi lo usa la libertà di concentrarsi sulle idee e sulla creatività e di tralasciare completamente i dettagli implementativi. Ad esempio, se io voglio creare una lista di oggetti in LISP, semplicemente scrivo: (LIST obj1 obj2 obj3) senza preoccuparmi di che tipo sono obj1, obj2 e obj3. In linguaggi imperativi come il C++ devo prima di tutto definire il tipo degli oggetti che devo inserire nella lista, diciamo che voglio una lista di interi (tipo che deve essere uguale per tutti gli oggetti nella lista), creare il contenitore lista: list<int> myList (ovviamente dopo aver specificato la libreria standard attraverso la dichiarazione dell'header #include <list> e dopo aver specificato che questo oggetto si trova nel namespace std), dichiarare e definire i tre oggetti: int obj1 = 3; int obj2 = 1; int obj3 = 5; inserire questi oggetti nella lista myList creata in precedenza: myList.push_back(obj1); myList.push_back(obj2); myList.push_back(obj3); e solo ora è possibile utilizzare l'oggetto lista.
Quello che però rende i programmi funzionali come il LISP veramente interessanti per la creatività di un programmatore è la capacità “introspettiva”. Un programma LISP può analizzare il suo stesso codice e modificarlo riscrivendone alcune parti, aggiungendo o rimuovendo funzionalità e tutto questo a run-time. Questa è la mia idea di AI, e secondo me è l'unica strada per raggiungerla: programmi che sono in grado di riscriversi da soli adattandosi ai cambiamenti non previsti dell'ambiente operativo in cui girano. Programmi che modificano programmi che modificano programmi. L'AI non può essere concepita da un design rigido fatto a tavolino, ma deve necessariamente essere un processo emergente.
Quest'anno il LISP compie 50 anni e sono sicuro che avrà ancora un ruolo fondamentale nei futuri sviluppi della computer-science, nonostante stiano prendendo piede anche altri linguaggi come il Python (van Rossum, 1980) che unisce assieme le caratteristiche dei linguaggi funzionali con quelle dei linguaggi imperativi.
Staremo a vedere, ma le prospettive sono decisamente interessanti.

Luca Ciciriello

martedì 9 settembre 2008

A systematic process

Parliamo un poco di grandi maestri. In questo blog ho già nominato Bjarne Stroustrup, sicuramente uno dei più grandi. Parliamo qui adesso di Andreas Zeller. Professore ordinario all'Universität des Saarlandes in Saarbrücken, Germania, è grazie a lui se noi oggi parliamo di “breakpoint”, “variable content”, “code completition”, o più in generale di debugger visuali. Infatti, assieme a Dorothea Lütkehaus, nel 1994 ha scritto il primo debugger visuale della storia. Nato come frontend del già famoso GNU gdb, “ddd” è sicuramente una conoscenza ben nota di chi abbia bazzicato un po' nella programmazione UNIX. Cosa molto più importante, Zeller ha definito una teoria scientifica del debugging. Riporto qui di seguito le sue parole tratte da uno dei suoi articoli più famosi (contenuto anche nello splendido libro che ogni programmatore dovrebbe leggere: “Beautiful Code”). Zeller dice:

“Quando i programmatori debuggano un programma, è per ricercare la causa di una failure. Questa può essere originata dal codice, dagli input o dall'ambiente dove il programma gira. Questa causa deve essere trovata ed eliminata. Una volta eliminata la causa, il programma riprenderà a funzionare. Se così non dovesse essere, nonostante aver eliminato la causa, è giunta l'ora di rivedere le nostre credenze sulla causa che ha generato la prima failure. Il processo generale di ricerca delle cause è chiamato 'metodo scientifico'. Questo metodo, applicato alle failures di un programma funziona come segue:

Si osserva la failure del programma.
Ci si costruisce un'ipotesi per la causa di questa failure che sia consistente con l'osservazione.
Si usa questa ipotesi per fare previsioni.
Si testano le previsioni sperimentando e facendo nuove previsioni.
Se la sperimentazione e l'osservazione soddisfano le previsioni, si rifinisce l'ipotesi iniziale, altrimenti bisogna trovare un'ipotesi alternativa.
Bisogna ripetere questo processo finché l’ipotesi iniziale non si possa più raffinare ulteriormente.

A volte le ipotesi possono diventare teorie. Questo vuol dire che abbiamo un 'Framework concettuale' che spiega e predice alcuni aspetti dell'universo. Infatti, il nostro programma malfunzionante può essere un aspetto veramente piccolo dell'universo, ma nonostante questo la nostra teoria ci può dire esattamente dove intervenire per eliminare la causa della failure.
Per ottenere questa teoria, i programmatori applicano il metodo scientifico descritto sopra ripercorrendo all'indietro la catena causa-effetto che origina la failure. Quindi essi osservano la failure (l’output è sbagliato oppure si origina un’eccezione), fanno ipotesi su quale potrebbe essere la causa della failure (la causa potrebbe essere che Y ha un valore errato), fanno una previsione (se Y è sbagliato, il suo valore potrebbe dipendere dalla funzione f() alla linea 632), testano la loro previsione (corretto! Y ha un valore errato alla linea 632), traggono le appropriate conclusioni (questo vuol dire che f() restituisce un valore errato e adesso vediamo perché).
Comunque, al di la di ogni metodo, espediente o trucco, l’uso consistente e disciplinato del metodo scientifico, è la chiave per raggiungere una certa maestria nel debugging. Questo significa tre cose:

Siate espliciti.
Formulate le vostre ipotesi esplicitamente. Scrivetele su un pezzo di carta o spiegate il problema ad un vostro collega. Tenere una traccia scritta del vostro problema vi può permettere di interrompere il lavoro e riprenderlo il giorno dopo a mente fresca.

Siate sistematici.
Siate sempre consapevoli di quello che state facendo. Non investigate e non fate cambiamenti a caso senza avere un’ipotesi chiara ed una previsione certa. Assicuratevi di aver preso in considerazione tutte le possibili cause.

Cercate le cause partendo dalle più probabili.
Il metodo scientifico vi garantirà di trovare la causa della vostra failure, ma non vi dirà quando. Quindi, come prima cosa identificate le possibili cause del vostro problema e concentratevi prima sulle più probabili e su quelle che richiedono lo sforzo minore.

Sfortunatamente, i debugger visuali come li conosciamo oggi, non supportano il metodo scientifico. Sicuramente questi sono strumenti estremamente utili per investigare il codice ed i risultati che produce, ma sono utilizzabili solo da programmatori con grande esperienza, che conoscono come usare sistematicamente un debugger. La mia idea, è quella di insegnare ai programmatori ad usare il metodo sistematico piuttosto che elaborati strumenti di debugging (…e un pochino mi sento colpevole avendo scritto io stesso un elaborato strumento di debugging)”.

Che dire. Dopo le parole di Zeller non mi rimane molto da commentare. Facciamo nostro l’insegnamento del maestro.
Parafrasando il motto di una vecchia pubblicità, posso solo dire: “Meditate, gente. Meditate!”

Luca Ciciriello

venerdì 5 settembre 2008

“There is no worse danger for a teacher than to teach words instead of things”

Siamo tutti concordi nell'affermare che il C++ (assieme all'Haskell, un linguaggio funzionale di ultima generazione, molto elegante) sia uno dei linguaggi più difficili da imparare, e da padroneggiare in maniera decente. Pochi dicono, però che il C++ è anche un linguaggio estremamente difficile da insegnare. Come tutti gli insegnanti di un linguaggio di programmazione, anche io, poco dopo l'inizio di un tipico corso di C++ mi sono trovato al fatidico punto di presentare il classico “Hello World”:

// Program Hello World
// File main.cpp
//
// To build use: g++ -o HelloWorld main.cpp

#include <iostream>
using namespace std;

int main(int argc, char *argv[])
{
cout << "Hello, World!" << endl;
return 0;
}

Già presentare un codice “semplice” come questo ad una classe di neofiti presenta delle enormi difficoltà. Da dove partire?
Qui le vie da seguire sono sostanzialmente due. La prima è quella di dire che molti degli oggetti presenti in queste 13 righe di codice verranno introdotti più avanti nel corso. La seconda è quella di spiegare ogni singolo oggetto che compone questo piccolo programma. Supponendo che si sia già spiegato cosa sia un Compilatore ed un Linker, e quale sia la differenza tra un programma interpretato ed uno compilato, bisognerebbe allora partire a spiegare in concetto di Translation Unit per poter introdurre il significato della parola chiave #include (il solo parlare del preprocessore del C++ richiederebbe un corso a parte). Il prossimo passo sarebbe poi quello di spiegare cosa sia una libreria (come quella che contiene iostream) e quindi l'utilità del lavoro svolto dal Linker. Si passerebbe poi a parlare di namespaces, ma per fare questo bisognerebbe parlare anche di “scopo”. A questo punto inizieremmo a fare i primi passi in quel territorio immenso che sono le funzioni per parlare di quella particolare funzione “main” e spiegare cosa sia l'entry point di un programma. Quindi inizieremmo a parlare delle differenze semantiche e sintattiche tra dichiarazione e definizione, tra parametri ed argomenti, quali sono i sistemi di passaggio degli argomenti ad una funzione visti sia dal punto di vista del programmatore sia dal punto di vista del compilatore e quale è l'ordine di lettura usato da una funzione per analizzare i suoi parametri (non vogliamo spendere un po' di tempo a spiegare anche il concetto di variabile?).
Scegliendo questa seconda via dovremmo poi parlare di operatori e di quale sia il significato di overloading, quindi del perché io posso usare lo stesso simbolo grafico per dire al compilatore di eseguire un'operazione di shift a sinistra su una serie di bit oppure di indirizzare un certo dato su uno stream di output. Ma allora a questo punto bisogna anche dire cosa sia uno stream e che ruolo giocano questi oggetti nel sistema I/O del linguaggio C++. Rimane ancora qualcosa da spiegare per far capire ad un neofita il significato di queste 13 linee di codice?
Ogni insegnante segue la sua via. Avendo il tempo necessario, io preferisco sicuramente la seconda. Prendendo come riferimento la frase di Stroustrup del post precedente, io preferisco spendere almeno un paio di giorni di lezione ripercorrendo la storia e le idee che hanno portato allo sviluppo del C++.
Spiego a fondo l'enorme importanza di seguire strettamente lo standard ISO/IEC, perché solo chi conosce a fondo e con maestria lo standard, può trascendere lo standard. Di questi programmatori ne esistono molto pochi e forse veramente solo Stroustrup può permettersi e soprattutto può osare, di trascendere questo standard. Chiunque altro dimostrerebbe solo presunzione (nella migliore delle ipotesi...) o ignoranza del linguaggio (...nella peggiore). Seguire uno standard non è un freno alla creatività, anzi fornisce delle basi solide sulle quali adagiare i pensieri della nostra fantasia. Ricordiamoci che un linguaggio di programmazione, non è solo descrittivo e formale, ma è anche espressivo (quando gli ingegneri inizieranno ad accorgersi di questo?).
Scegliendo la prima delle due vie descritte qui sopra, mi sembrerebbe un po' di correre il rischio paventato nella frase di Marc Block che titola questo post. Una parola può cambiare l'universo se sostenuta dalla verità, ma è solo vuoto se non serve a trasferire o creare significato.

Luca Ciciriello