Articoli

Dal GOTO ai Webservices

Indice

 

La storia

1968: io non ero ancora nato, ma già Dijkstra aveva fatto sapere al mondo che era meglio non usare l'istruzione "GO TO". 1973: la mia nascita. Tutto il mondo concordava sul fatto che Dijkstra aveva ragione, ma molti preferivano ancora ignorare la cosa. 1985: il mio primo computer, il primo primo GO TO. Lo usai senza sapere che 17 anni prima Dijkstra aveva deprecato questa deplorevole pratica di programmazione.

Due (o tre) parole sul GO TO

Andiamo per passi. Per chi è giovane e digiuno di programmazione vecchio stile, dirò due parole su cosa sia l'istruzione "GO TO". Come la traduzione dall'inglese ("vai a") fa intuire, serve a dire al computer di spostarsi. Beh, non proprio nel senso fisico del termine. Un programma è una sequenza di istruzioni che il computer esegue come se fossero operazioni matematiche; le esegue una dopo l'altra, dalla prima istruzione del programma fino all'ultima.

Spesso però è conveniente fare in modo che il computer non esegua le istruzioni nello stesso ordine in cui sono state scritte dal programmatore, ma le esegua in un ordine diverso, o ne ripeta una porzione per un po' di volte, o ancora ne eviti alcune per proseguire più avanti nell'elenco. Queste modifiche all'ordine di esecuzione si chiamano cambi di flusso. A meno che il programma non sia qualcosa di assolutamente banale, ne conterrà certamente parecchi.

I linguaggi a basso livello

Il computer è terribilmente stupido. Il paragone con la calcolatrice è quantomai azzeccato. Non ha coscienza di sè, non sa prendere decisioni e non sa discernere un volt da un ampére (o forse questa è una delle poche cose che sa fare, ma a sue spese...). Le istruzioni che esegue sono talmente semplici ed inequivocabili da poter essere eseguite da qualsiasi entità stupida come un computer. Alcuni esempi di istruzioni che esegue:

 Appuntati qui il valore 53
Somma al valore che ti sei appuntato qui quell'altro valore che ti eri appuntato là
Scriviti il risultato laggiù

Eccetera. Ovviamente queste istruzioni sono talmente banali che per tirarne fuori qualcosa di utile è necessario che il computer ne esegua alcuni milioni al secondo e non sempre nello stesso ordine. Il flusso del programma deve cambiare in base ai valori che il computer si è calcolato precedentemente. Ecco che entra in gioco l'istruzione che fa cambiare il flusso del programma. Vediamola con un nuovo esempio:

 Leggi il valore che ti eri scritto là a destra
Sottrai 7 al valore che hai letto
Se il risultato è minore o uguale a 0 evita di eseguire le prossime 2 istruzioni
Scrivi il risultato là a destra al posto del valore precedente
Riprendi ad eseguire dalla prima istruzione
Se il risultato è pari a zero significa che il valore iniziale è divisibile per 7

Questo programma in linguaggio naturale (nel caso specifico è italiano) istruisce il computer su come verificare se un numero intero sia divisibile per 7. Se scrivessimo questo programma in linguaggio assembly, ovvero il linguaggio di programmazione più vicino alla stupidità del computer, scriveremmo qualcosa del genere:

Inizio:
MOV EAX, [destra]
SUB EAX, 7
JLE [fine]
MOV [destra], EAX
JMP [inizio]
fine:
JZ [divisibileper7]

Le istruzioni assembly sono in corrispondenza uno a uno con le istruzioni in linguaggio naturale elencate sopra. EAX è una delle variabili che il computer usa per tenere a mente dei valori temporanei su cui sta lavorando. MOV EAX, [destra] significa "prendi il valore che c'è là a destra e mettitelo nella variabile EAX, così da poterci lavorare". JLE significa "Jump if Less Or Equal". JMP significa "jump" ovvero salta incondizionatamente all'inizio del programma. JZ significa "Jump if Zero": salta ad un punto del programma, che qui non ho definito, e che fa qualcosa di presumibilmente utile, se l'ultima operazione matematica ha dato zero come risultato, il che, in questo caso, coincide con il numero iniziale divisibile per 7. Semplicemente per verificare la divisibilità per 7 di un numero intero, per di più con un rudimentale metodo tutt'altro che ottimizzato o veloce, abbiamo usato ben tre istruzioni di cambio di flusso.

I linguaggi ad alto livello

Dall'esempio fatto è evidente che il linguaggio assembly è troppo vicino alla stupidità del computer per essere comodo da usare. Quando i programmatori scrivono un programma vorrebbero poter pensare solo al problema che il programma dovrà risolvere, senza doversi ricordare dettagli appartenenti alla stupidità del computer, come il fatto che per poter sottrarre 7 ad un numero bisogna prima copiare il numero nella variabile EAX. Per questo motivo tempo fa (molto prima che io nascessi) i programmatori inventarono linguaggi di programmazione ad alto livello.

Un linguaggio ad alto livello è composto da parole (ovvero istruzioni) il cui significato è di più alto livello, ovvero più vicino al modo di pensare umano e più lontano dal modo di funzionare del computer. Prima che il computer possa eseguire le istruzioni di un linguaggio ad alto livello, queste devono essere tradotte in linguaggio assembly (anzi, per la precisione in linguaggio macchina, abbr. LM, che è ancora peggio). Ogni istruzione ad alto livello viene tradotta in un gruppo di istruzioni equivalenti in LM, che si prendono cura dei dettagli che i programmatori si rifiutano giustamente di ricordare. BASIC, Pascal, FORTRAN, COBOL sono solo alcuni dei linguaggi di programmazione ad alto livello che furono inventati.

Tutti questi linguaggi avevano in comune alcune caratteristiche, fra cui la presenza dell'istruzione "GO TO", che sostanzialmente veniva tradotta nell'istruzione JMP a livello assembly o LM. Il GO TO era quindi l'istruzione che serviva a far cambiare il flusso di esecuzione del programma, facendo saltare la CPU ad un'istruzione che, in base all'ordine di scrittura del codice, non era quella immediatamente successiva. L'esempio fatto prima, scritto in un ipotetico linguaggio BASIC, diventerebbe:

    1 valore = valore - 7 
2 IF valore <= 0 THEN GO TO 4
3 GO TO 1
4 IF valore = 0 THEN GO TO 45328

Perché il GO TO è dannoso

Chiaramente in questo esempio, come nei precedenti, il programma è incompleto, in quanto non si capisce da dove venga effettivamente preso il valore iniziale, nè si dice quale sia l'istruzione numero 45328. Salta però subito all'occhio un fatto: in BASIC sono bastate quattro istruzioni, mentre in assembly ne servivano sei. Inoltre le quattro istruzioni del BASIC sono più semplici da capire per gli umani delle sei istruzioni assembly. Quel che rende questo programma piuttosto criptico ora non sono più le istruzioni, ma il flusso di esecuzione. Supponiamo di non sapere che il programma è stato scritto per verificare la divisibilità per 7 di un numero intero. Cerchiamo di intuirlo simulandone l'esecuzione da parte del computer. Si parte dall'istruzione 1, dove il valore viene diminuito di 7. Per ora non sappiamo il perché. All'istruzione 2, se il nostro valore è minore o uguale a zero, si salta all'istruzione 4. Notate che all'istruzione 2 non c'è scritta la motivazione logica per cui si esegue quel salto, perché quell'istruzione serve solo ad interrompere il ciclo di sottrazioni di 7 dal valore. All'istruzione 3 capiamo che le prime due saranno eseguite fino a quando qualcosa non interromperà l'armonia delle ripetizioni: l'unica cosa che può interrompere tale ciclo di esecuzione delle righe da 1 a 3 è il GO TO che c'è alla riga 2, quindi capiamo che tali righe saranno eseguite ripetutamente fino a quando il valore non arriverà a zero o meno di zero. All'struzione 4 capiamo che se il valore è esattamente zero il programma si comporta in modo diverso dagli altri casi. Quindi ora abbiamo capito che il programma sottrae 7 al valore fino a quando questo arriva a zero o meno di zero e se arriva esattamente a zero si comporta in modo diverso dagli altri casi. Con un po' di intuizione capiamo dunque che il programma si comporta in modo diverso quando il valore iniziale è divisibile per 7. Insomma per capire cosa fa questo programma dobbiamo pensarci un po' e saltare da un'istruzione all'altra con la mente. Il GO TO è dannoso. Lo disse Dijkstra e aveva ragione.

L'alternativa al GO TO

Il problema del GO TO è che è poco espressivo. Tant'è vero che corrisponde ad una sola istruzione LM. Probabilmente non avrebbe mai dovuto esistere in un linguaggio ad alto livello. Comunque nei linguaggi ad alto livello ci sono alternative, come, per esempio, l'istruzione GO SUB o CALL (il nome cambia a seconda dei linguaggi). Questa istruzione permette di far saltare il flusso ad un certo punto per poi farlo ritornare all'esecuzione dell'istruzione immediatamente successiva al salto. In questo modo è possibile scrivere delle subroutines, ovvero sequenze di istruzioni che tornano utili più volte all'interno del programma e che possono essere richiamate da vari punti del programma stesso. Alla fine della subroutine l'istruzione RETURN segnala al computer che è ora di tornare all'istruzione immediatamente successiva alla GO SUB o CALL. Il vantaggio evidente, dal punto di vista della leggibilità del codice, è che, una volta che ho capito cosa fa la subroutine, non dovrò più andarla a guardare tutte le volte che nel codice ci sarà un salto ad essa, ma potrò considerarla come una macro istruzione e proseguire con la lettura del codice seguendo il filo logico del programmatore che lo ha scritto. Se poi il linguaggio permette di assegnare un nome alle subroutines invece che un semplice numero, il codice diventa ancora più leggibile, perché il nome darà un'idea immediata seppur grossolana di cosa fa la subroutine.

Ai programmatori però non bastava ancora. Come mai? Vediamolo con un esempio.

    INPUT numero    
divisibilePer7 = "no"
GO SUB check_divisibilePer7
IF divisibilePer7 = "si" THEN PRINT "E' divisibile per 7"
END
check_divisibilePer7:
numero = numero - 7
IF numero > 0 THEN GO TO check_divisibilePer7
IF numero = 0 THEN divisibilePer7="si"
RETURN

Il programma è ora completo. Chiede il numero all'utente con l'istruzione INPUT, chiama la subroutine che verifica la divisibilità per 7 e che, nel caso, imposta a "si" la variabile apposita, poi verifica la variabile e stampa un messaggio all'utente ed infine termina. Ora supponiamo che il programmatore che ha scritto questo programma si trovi a doverne scrivere un altro e che anche nell'altro debba verificare la divisibilità per 7 di un numero. Il programmatore ovviamente non ha nessuna intenzione di riscrivere la subroutine, quindi prende il listato di questo programma e copia pedestremente la subroutine nel listato del nuovo programma. Poi fa lo stesso con un terzo programma ed un quarto, nel quale però deve verificare la divisibilità per 19, quindi oltre a copiare apporta una piccola modifica. Questo programmatore ora si trova con quattro copie della sua subroutine. Inoltre un suo collega ha bisogno della stessa subroutine, ma nel suo programma la variabile "numero" è già usata per qualcos'altro, quindi la subroutine viene ulteriormente modificata per usare una variabile con nome diverso e copiata nei programmi del collega.

Un triste giorno il programmatore trova un errore di programmazione nella sua subroutine (in questo caso specifico, se il numero è negativo lo darà sempre come non divisibile). Deve correggerne 4 copie sue e farne correggere chissà quante copie al suo collega (sperando di ricordarsi tutti i programmi in cui l'aveva copiata). Sarebbe stato meglio se la subroutine fosse esistita in un solo punto del mondo e se tutti i programmi se la fossero andata a prendere direttamente da lì. Questo pone però due problemi: primo, per esistere in un solo punto deve esisterne una sola versione, non due versioni di cui una verifica la divisibilità per 7 e l'altra la divisibilità per 19. Secondo, per esistere in un solo punto non può esistere in nessun programma particolare, ma deve stare fuori da qualsiasi programma specifico in modo che tutti i programmi sappiano dove si trova la subroutine e possano fare riferimento ad essa per usarla.

Le funzioni

Il primo problema fu risolto trasformando la subroutine in una funzione. Una funzione è simile ad una subroutine, ma, in più, accetta delle variabili in input che sono dette parametri della funzione e restituisce dei valori in output, detti valori di ritorno. Nel nostro caso la funzione accetterebbe due parametri, uno è il numero di cui si vuole verificare la divisibilità e l'altro è il divisore. Restituirebbe un valore vero/falso (cioè di tipo booleano), che sarebbe vero se il numero è divisibile per il divisore, falso altrimenti. In questo modo la funzione esiste in un'unica versione e si adatta a tutti i numeri e divisori.

function check_divisibile
param numero integer
param divisore integer
numero = numero - divisore
IF numero > 0 THEN RETURN check_divisibile numero divisore
IF numero = 0 THEN RETURN VERO
RETURN FALSO

Si noti che al posto del GO TO è stata inserita una chiamata ricorsiva alla funzione stessa, ovvero la funzione richiama sé stessa con valori modificati rispetto a quelli che il programma usa quando la chiama per la prima volta. Questo è possibile solo ora che la subroutine è diventata una funzione ed accetta parametri. Leggendo il codice attuale è decisamente più chiaro quello che fa, anche senza saperlo a priori. Anche il modo in cui lo fa è chiaro, dato che non c'è più il GO TO, ma al suo posto c'è una chiamata di funzione decisamente più espressiva.

Il secondo problema, ovvero il fatto di mettere la funzione fuori da qualsiasi programma, fu risolto nel modo più ovvio, ovvero scrivendo le funzioni di comune utilità in files separati da quelli in cui venivano scritti i programmi e poi inserendo all'inizio dei programmi un riferimento ai nomi di files che contenevano le funzioni che il programma usava. Questi files separati presero il nome di librerie.

Non è sempre tutto previsto

Su queste basi si sviluppò software fino all'alba del nuovo millennio. E lo sviluppo avrebbe anche potuto continuare così, ma nel 1992 il World Wide Web, ovvero Internet, divenne una realtà a fronte di tante utopie teorizzate durante il ventesimo secolo. Internet vide crescere vertiginosamente il suo sviluppo a partire dal 1995, probabilmente a causa di vari fattori coincidenti fra cui il rilascio da parte di Microsoft di Windows 95, che includeva Internet Explorer preinstallato assieme al sistema operativo.

Nei primi anni del nuovo millennio si iniziò di nuovo col formulare nuove utopie, perché la vastità di Internet non bastava più. O forse non solo bastava, ma era decisamente troppo da poter gestire tutta assieme. O ancora, l'uomo non è contento se non deve risolvere dei problemi, quindi la specie uomo iniziò a teorizzare un nuovo modo di usare Internet che creasse sufficienti problemi da risolvere almeno per la prima metà del ventunesimo secolo. L'idea questa volta si chiama Web 2.0, così battezzata da O'Reilly.

Nella nuova Internet non ci sono solo milioni di siti ed ognuno fa per sè, ma ogni sito offre dei servizi, non solo utilizzabili da individui umani come era nella vecchia internet, ma anche utilizzabili da altri programmi. Ogni sito se vuole può interagire con altri siti, scambiare informazioni, riorganizzarle, unirle e presentarle ai suoi utenti in modo nuovo. Ogni sito non è più un insieme passivo di documenti da leggere farciti di qualche form per fare un'ordine online, ma è un vero e proprio applicativo che offre funzionalità specifiche ai suoi visitatori, siano essi umani o altri siti. Ogni sito offre quindi una libreria di funzioni ed usa le librerie di funzioni degli altri siti. In questa visione del Web 2.0, a metà fra l'ambizioso e l'utopico, nascono i Webservices, ovvero uno standard che dice "in quale lingua" si devono parlare fra di loro i vari siti.

I Web Services

Ora immaginiamo di entrare cinque anni fa nel mondo del lavoro, freschi di laurea in ingegneria informatica o qualcosa del genere, e ci venga chiesto di scrivere le specifiche tecniche del Web 2.0.

Se non sappiamo nulla di Dijkstra e delle sue idee, potremmo immaginarci che i siti si parlino fra loro a colpi di GO TO. I programmatori dei due siti che si devono parlare dovrebbero mettersi d'accordo su come si usano i servizi dei rispettivi siti. Diciamo che sito_2 mette a disposizione il codice per verificare la divisibilità per 7, mentre sito_1 crea l'interfaccia utente ed usa il codice di sito_2. Il risultato sarebbe qualcosa del genere:

sito_1:
Chiedi all'utente un numero
Invia il numero all'indirizzo di sito_2 con protocollo TCP sulla porta 35446

Chiaramente "Invia il numero..." corrisponde al vecchio GO TO, perché è un salto ad un indirizzo, ad un numero di porta preciso e leggendo il codice non si sa cosa capiti a quell'indirizzo sul quel numero di porta. Con questo schema, sito_2 dovrebbe dare il risultato del controllo di divisibilità per 7 in modo altrettanto criptico, indipendentemente dal fatto ch usi la stessa connessione o un'altra. Il codice di sito_1 resterebbe comunque incomprensibile. Tuttavia ogni neolaureato che si rispetti sa che la prima regola è non reinventare l'acqua calda, quindi ha studiato e sa che il GO TO non va bene. L'idea buona è subito la libreria di funzioni.

sito_1:
Chiedi all'utente un numero
Usa la libreria "matematica_di_base" disponibile su sito_2
Chiama la funzione check_divisibile numero 7

Bene, problema risolto. Ora i siti possono parlarsi. Che facciamo adesso? Ci guardiamo negli occhi mentre tutto funziona? Che noia! No tranquilli, da fare ne abbiamo. Pensavate forse che quelle tre righe di codice fossero l'attuale realtà dei Webservices? Peccato, la questione è ancora molto più complicata. Quelle tre righe sono il punto a cui arriveremo forse a metà del ventunesimo secolo.

Portare il modello della libreria di funzioni sul web significa fare in qualche modo riferimento a quella libreria nel codice che scriviamo. Così come nella programmazione classica nel codice si aggiunge un riferimento al nome del file della libreria, allo stesso modo nella programmazione dei Webservices dobbiamo aggiungere un riferimento al nome del sito.

Non basta, ogni sito può pubblicare più liberie, quindi, mentre nella programmazione classica il nome del file identifica in modo univoco la libreria, ora dobbiamo specificare, oltre a quale sito, anche quale libreria. Poi quale funzione e quali sono i parametri.

Per ora quindi per chiamare un Webservice il codice è un po' più lungo della semplice chiamata di funzione. Nella migliore delle ipotesi (caso semplice) sono una quindicina di istruzioni effettive che in Java si sviluppano in una trentina di righe di codice. Avete capito bene: trenta righe per fare quello che fino al 1967 si faceva con un GO TO. Non ci credete? Ve lo dimostro con un esempio. Riporto qui sotto il codice Java (uno dei linguaggi preferiti al giorno d'oggi per programmare con i webservices) necessario per chiamare un ipotetico webservice usando le librerie di Axis2:

                                        new URL("http://ws.sito.com/checkdivisibile.wsdl"),

In tutto questo siamo riusciti a chiamare una funzione sul sito ipotetico ws.sito.com, usando la crème de la crème della tecnologia attuale, ovvero Axis2, che è forse il miglior strumento a disposizione per programmare con i webservices. Facile no?

Quello che sarebbe bello poter fare, ma che la teconogia di oggi ancora non permette, sarebbe qualcosa del genere (e chissà che magari qualche anima pia vedendo questo mio esempio non decida di inventare e creare gli strumenti necessari per arrivare a questa semplicità):

public class ChiamaUnWebservice
{

public ChiamaUnWebservice()
{
}

public void consume() throws Exception
{
boolean divisibile = WebServices.ws.sito.com.math.checkDivisibile(49, 7);
}

public static void main(String[] args) throws Exception
{
ChiamaUnWebservice cuws = new ChiamaUnWebservice();
cuws.consume();
}
}

Diciamo tutta la verità

Non è che si sia poi tanto distanti dall'ipotetica soluzione semplice che ho presentato qui sopra. I maggiori ambienti integrati di sviluppo Java (attualmente Eclipse e Netbeans) mettono a disposizione delle funzionalità di generazione automatica del codice che permettono di avere le trenta righe (in realtà molte di più) generate automaticamente, lasciando al programmatore il solo il compito di chiamare il webservice quando il suo codice ne ha bisogno. Tuttavia questa soluzione non è molto pulita. Il codice generato automaticamente è soggetto a decisioni prese automaticamente dallo strumento che lo ha generato, ed è tipicamente poco leggibile. Quando qualcosa non funziona (e nei programmi c'è sempre qualcosa che non funziona) andare a trovare l'errore nel proprio programma che, fra le altre cose, fa uso di un ginepraio generato automaticamente, non è il massimo del piacere.

Il doveroso rant finale

Sono d'accordo con voi, quello che c'è da fare per far parlare due siti è molto di più di quello che è possibile fare con un GO TO, ma il problema torna ad essere quello dei linguaggi a basso livello. Mentre scriviamo quelle 15 istruzioni non stiamo pensando alla business logic, non stiamo risolvendo il problema che il nostro software (ormai un sito) deve risolvere, ma stiamo subendo la stupidità dei computer. Purtroppo ho sempre più l'impressione che l'informatica attuale sia come l'industria dei primi anni dell'ottocento. Si muove come un elefante in una cristalleria, con tentativi grossolani che spesso fanno più danni che altro, ma che sono purtroppo necessari per il progresso. Tentativi che ci fanno guadagnare a piccole porzioni il traguardo che raggiungeremo fra molto tempo. Per guadagnare la potenza e flessibilità dei Webservices paghiamo un prezzo altissimo, tornando a programmare come si faceva all'inizio, ovvero occupandoci della stupidità dei computer invece che dei nostri problemi. È come se stessimo programmando in linguaggio assembly. E quanti anni ci sono voluti perché l'informatica passasse dal linguaggio assembly alla programmazione ad oggetti? Mezzo secolo. Quanti ce ne vorranno per inventare un nuovo paradigma di programmazione che risolva di nuovo lo stesso problema? Spero meno, ma, nel dubbio, per il prossimo mezzo secolo mi metto il cuore in pace.