Ancora su documentazione e tests

Ancora su documentazione e tests

E’ bello scatenare una guerra di religione in un forum moderato perche’ poi alla fine parlando senza dichiarare guerre sante si arriva a focalizzare meglio i concetti. Nel caso dell’articolo che ho scritto sulla programmazione, la questione della documentazione, dei javadoc e dei godoc ha toccato alcuni nervi scoperti. In particolare, quelli di chi dice che “il codice si documenta da solo” e di chi dice che “i test sono documentazione”. Vediamo di fare due esempi.

Prendiamo la prima affermazione, “il codice si documenta da solo”, e facciamo un esempio abnorme usando un linguaggio abnorme, in questo caso Perl.

Guardiamo che meraviglia di modo che si usa per documentare “map”:

my @names = qw(Foo Bar Baz);
my @invited = map {$_ => 1} @names;
print "@invited\n"

sembra bello, giusto? Funziona benissimo, se abbiamo tre invitati. E funziona benissimo anche con 100 invitati.
Domanda: se ci faccio passare un file di log di 200MB, mappando una riga per elemento dell’array, che succede?

Beh, chi conosce Perl sa bene che impieghero’ secoli per manipolarlo. La ragione e’ legata al fatto che map produrra’ innanzitutto una copia dell’intero array sulla quale lavorare, e ne fara’ una diversa ad ogni invocazione di map. Se quel “map” su grandi file io lo infilassi in un ciclo di loop, cioe’, sarei nella cacca pura.

Ma c’e’ di peggio. Guardiamo un attimo la funzione “grep” di perl.

Viene documentata , per fare un esempio, cosi’:

 #!/usr/bin/perl
    use strict;
    use warnings;

    my @strs = qw(potatoes lemons apricots apples bananas ostriches flamingoes);
    my @short_strs = grep { length $_ < 8 } @strs;
    for my $str (@short_strs) {
        print "$str\n";
    }

Ora, sinche’ avete roba del genere, funziona. Ma ripeto la domanda: se io ci sbatto dentro un file di log da 200MB, funzionera’ leggendo ogni riga come un elemento dell’array? Sapete bene che avro’ problemi ancora peggiori che con map: non solo perl costruira’ una copia del file in memoria, ma lo fara’ ogni volta che viene invocato grep.

In pratica abbiamo qualcosa (grep, map) che POSSONO essere invocate su un array da 200MB di stringhe. E’ sintatticamente corretto. Ma non e’ consigliabile farlo.

Quando vado a pubblicare il mio codice come documentazione, pero’, io questo non lo leggo. Leggo un interessante esempio con 7 stringhe, ma non vedo scritto che non possano essere 14 milioni, da nessuna parte. Leggendo il codice che si documenta da solo sto leggendo la sintassi , so come invocare grep e map, ma NON ho la piu’ pallida idea di come funzioni la cosa. Otto vanno bene. E nove? Vanno bene. E diciotto milioni? Anche. OOPS.

Ogni pezzo di codice fara’ delle cose in un certo modo. Questo modo avra’ dei pro e dei contro. Ad ogni “contro” corrisponde un’azione che, se anche e’ sintatticamente corretta, NON e’ per nulla consigliabile.

Il problema del dire che il codice “si documenta da solo” e’ che si “documenta da sola” la sintassi. Niente altro che la sintassi. Quando si va a vedere se per caso stiamo anche usando il codice come lo aveva pensato chi lo ha scritto, la semplice sintassinon ce lo dice.

Certo, conoscendo perl sappiamo che non e’ il modo giusto di fare il parsing di un file da 200MB. Ma se conosciamo POCO il perl, allora sappiamo che “perl e’ fantastico per manipolare stringhe”. Lo dicono tutti, giusto? La prima cosa che ci viene in mente di fare per cercare una riga di log in un file , ovviamente, e’ usare la grep di perl esattamente come usiamo la grep di unix.

Quindi, non solo usare il codice come documentazione e’ un errore per via dell’incompletezza: e’ un errore per via dell’ambiguita’ .

Andiamo ad un altro punto: “il test e’ una documentazione sufficiente”. No, non lo e’.E per dimostrarlo scrivero’ un codice che ha gigantesche limitazioni concettuali, quasi invisibili sui test, che al massimo mostreranno dei fallimenti puntuali. E lo scrivero’ semplice. Facile da leggere.

Allora, prendiamo questa formula :

derivata

e trasformiamola bovinamente in c:

// "derivata" della stessa 

double derivata( double icszero )
    {
    double d1,d,acca;
    d=0;d1=1;
    acca = 1;
    while ( d != d1  ) //vabe', almeno proviamoci
        {           
        d1 = d; 
        d=(funzione(icszero+acca)- funzione(icszero))/(acca);
        acca = acca / 2.0; 
        }
    return d;
    }



Come vedete, a leggerlo il codice fa ESATTAMENTE quello che dice la formula. Ha senso. Con una scelta oculata dei nomi delle variabili, si pronuncia persino allo stesso modo. Sul piano del codice, potete dire quel che volete, ma e’ esattamente l’esecuzione di quella formula. Quindi, il codice dice che sto facendo quella roba.

Certo, se potessi documentare dovrei dire che la funzione NON va a fare alcun controllo sulla derivabilita’, dunque della continuita’. Ma non posso, e quindi dovete capirlo da soli, oppure vedete una formula implementata cosi com’e’.

Allora mi direte che se scriviamo dei test, allora capiremo tutto. Ok.

Allora:

double funzione(double x)
    {
    return x*x ;
    }

Ehi, funziona! Se deriviamo per x=3, ci restituisce 6! Successone! Siamo gia’ pronti a lanciarci nel mondo del meraviglioso calcolo differenziale! Siamo dei leoni.

E allora proviamo ancora e….

double funzione(double x)
    {
    return sin(x);
    }

Scriviamo il test per x=1 e anche questo va! Fantastico. Siamo gia’ pronti ad arruolarci nella Dinamo RungeKutta. Ma come ho scritto, questa roba funziona bene nell’insieme dei reali, e funziona un pochino peggio nel mondo dei numeri discreti.

E allora adesso mettiamo i nostri test negativi. Scriviamo il nostro bel “failing_test.c” e ci mettiamo i casi in cui fallisce. Uno e’ ovvio.

double funzione(double x)
    {
    return (x != 3)? 1.0 : 0.0 ;
    }

E siccome siamo personcine amabili, andiamo a calcolare la “derivata” nel punto x = 3.

Come vedete, il risultato e’ sbagliato. (ovvio, visto che non ne esiste uno giusto). Ma quel che e’ peggio lo potrete vedere stampando, iterazione dopo iterazione, i valori di “d” e di “acca”: terrificante, vero?(1) Specialmente quel “saltino” da 4.611.686.018.427.387.904 a … zero, che trovo commovente: del resto stiamo stampando l’inverso di un numero piu’ piccolo dell’epsilon di macchina…

Faccio presente, pero’, che se avessi calcolato la derivata in QUALSIASI altro valore di x, non si sarebbe notato nulla. Avrei potuto scriverlo ANCHE nei test che si concludono con successo!

Adesso, chi ha studiato sa cosa stia succedendo ma… voi non scrivereste una cosa simile. Pero’ siamo al punto in cui chi legge NON SA , e deve capire leggendo i test. Quanto spiega questo test fallito, a chi non aveva capito il problema da subito?

Quando io avessi mostrato al tizio che trova su google il nostro sito e vuole un metodo rapido per fare la derivata (che in questo esempio fallisce), cosa ci ha capito? Cosa capisce da questo test, che e’ chiarissimo praticamente per chiunque programmi?

Se potesse capire, si sarebbe scritto lui lo stesso codice. Se ha bisogno di un codice del genere, e non ne capisce i limiti a guardarlo, che cosa dedurra’ dal fatto che adesso la funzione fallisce?

Se siete fortunati, dira’ una cosa come “si ma le derivate sono per le funzioni coi numeri, mica per gli if. Mica e’ programmazione.” Sentito con le mie orecchie.

Cosi’, diciamo che di quelli che trovano questo codice su google, circa 30 non hanno capito i suoi limiti, e lo useranno per calcolare la derivata di abs(x) in x=0. Gli altri eviteranno, perche’ hanno capito il concetto dal primo esempio.

Ma i nostri trenta pensano che se non ci sono if o altri costrutti “da programazione” la derivata vada bene. Cosi’ proviamo a proporre questo test fallito, con una funzione semplice, senza if, for, etc. In modo che anche lui possa capire:

double funzione(double x)
    {
    return sin (1/x);
    }

scriviamo il nostro secondo test, chiediamo ‘derivata(0)’ e mostriamo che questa roba fallisce. Siamo ancora nelle condizioni mancanti di verifica di derivabilita’ e nelle stesse condizioni di comprensione della funzione, ma adesso non c’e’ nessun if. Quindi il nostro eroe che ha imparato a programmare “per il web” , che cosa capisce da questo fallimento? Quasi nulla. Diciamo che abbiamo qui una settantina di quei 100 che lo useranno senza ritegno , anche in condizioni di cuspidi, discontinuita’ di ogni genere, e compagnia bella.

Cosi’ facciamo un esempio ancora piu’ semplice con una cosa semplicissimissima.

E scriviamo un altro test fallito, e siccome non siamo figli di maria, sempre per x=3:

double funzione(double x)
    {
    return (1/x);
    }

voi direte: ma perche’ dici che fallisce?

Mettiamola cosi’: e’ concepito per amplificare l’errore di macchina. Sul mio compilatore c, in questo momento, usando un double restituisce -0.11111110448837280273 , credendo che sia –19 . Per aver usato dei “double” potremmo avere di meglio, ma anche usando dei long double, otteniamo -0.11111111112404614687! Un po’ schifino, diciamo. Si, i primi decimali sono corretti, ma per essere –19 fa “schifetto”. Non ha proprio fallito-fallito: ha mostrato i propri limiti di precisione, ecco.

Sono curioso di sapere cosa restituisca in altri linguaggi, ma a meno che i vostri compilatori siano molto furbi, oltre questo non mi aspetto nulla di buono. Quell’algoritmo fa schifo.

Allora, che succede? Abbiamo scritto un pezzo di codice, che sul piano del codice sembra fare esattamente quello che dice la formula matematica. A dire il vero lo fa, con il piccolo dettaglio che siamo in un insieme discreto e non nel mondo dei reali (motivo per il quale l’algoritmo termina, btw).

Adesso riflettiamo e chiediamoci, di quei 100 che ci hanno trovato cercando su github un metodo veloce per fare le derivate, quanti hanno capito i tre test falliti. Risposta: quelli che possono capire i tre test falliti il codice se lo scrivono da soli, sono poche righe. Tutti gli altri, da quei test falliti NON CAPIRANNO NIENTE.

Che lavoro fanno quelli che non capiscono? Oggi come oggi, potenzialmente sono ovunque. Banche, assicurazioni, ospedali, ovunque. E se pensate che ADA possa fermare il problema, vi sbagliate di grosso: la stessa funzione scritta in ADA nello stesso modo ha gli stessi identici problemi che mi sono cercato.

Se potessimo scrivere documentazione potremmo dire chiaramente che stiamo calcolando una derivata “destra” , e che in un ambiente discreto in alcuni casi tutto si riduce alla stampa di un inverso dell’epsilon di macchina con una verifica che arriva troppo tardi. Potremmo scrivere che non facciamo controlli sulla derivabilita’.

Ma avendo solo il codice e i test, posso scommettere quanto volete che un sacco di persone ricicleranno questa funzione, credendo che in fondo si, qualche volta non funziona, ha qualche problema di precisione (LOL) ma in fondo, si, i numeri che deve macinare li macina.

Quindi no: ne’ il codice ne’ i test possono sostituire la documentazione. I limiticoncettuali di un algoritmo NON sono spiegabili con dei test , positivi o negativi che siano. Serve proprio la documentazione.

Se qualcuno prendesse quel pezzo di codice sopra e iniziasse ad usarlo per fare cose serie, i risultati potrebbero essere comici. I problemi salterebbero fuori solo in alcuni casi, gli errori di accumulerebbero lentamente, sino a che …. paf. Il vostro conto in banca sparisce.

Il codice pericoloso NON viene fermato ne’ leggendo il codice ne’ leggendo i test. Anche quando i test falliscano, e noi forniamo i test che falliscono, SOLO ALCUNIavranno capito perche’ falliscono e quali altri casi analoghi fallirano.

Il resto, se il codice e’ abbastanza complicato, non capira’ affatto il problema. Si limiteranno ad usare un codice che e’ limitatissimo.

(1)

acca 1.00000000000000000000 , D:1.00000000000000000000
acca 0.50000000000000000000 , D:2.00000000000000000000
acca 0.25000000000000000000 , D:4.00000000000000000000
acca 0.12500000000000000000 , D:8.00000000000000000000
acca 0.06250000000000000000 , D:16.00000000000000000000
acca 0.03125000000000000000 , D:32.00000000000000000000
acca 0.01562500000000000000 , D:64.00000000000000000000
acca 0.00781250000000000000 , D:128.00000000000000000000
acca 0.00390625000000000000 , D:256.00000000000000000000
acca 0.00195312500000000000 , D:512.00000000000000000000
acca 0.00097656250000000000 , D:1024.00000000000000000000
acca 0.00048828125000000000 , D:2048.00000000000000000000
acca 0.00024414062500000000 , D:4096.00000000000000000000
acca 0.00012207031250000000 , D:8192.00000000000000000000
acca 0.00006103515625000000 , D:16384.00000000000000000000
acca 0.00003051757812500000 , D:32768.00000000000000000000
acca 0.00001525878906250000 , D:65536.00000000000000000000
acca 0.00000762939453125000 , D:131072.00000000000000000000
acca 0.00000381469726562500 , D:262144.00000000000000000000
acca 0.00000190734863281250 , D:524288.00000000000000000000
acca 0.00000095367431640625 , D:1048576.00000000000000000000
acca 0.00000047683715820312 , D:2097152.00000000000000000000
acca 0.00000023841857910156 , D:4194304.00000000000000000000
acca 0.00000011920928955078 , D:8388608.00000000000000000000
acca 0.00000005960464477539 , D:16777216.00000000000000000000
acca 0.00000002980232238770 , D:33554432.00000000000000000000
acca 0.00000001490116119385 , D:67108864.00000000000000000000
acca 0.00000000745058059692 , D:134217728.00000000000000000000
acca 0.00000000372529029846 , D:268435456.00000000000000000000
acca 0.00000000186264514923 , D:536870912.00000000000000000000
acca 0.00000000093132257462 , D:1073741824.00000000000000000000
acca 0.00000000046566128731 , D:2147483648.00000000000000000000
acca 0.00000000023283064365 , D:4294967296.00000000000000000000
acca 0.00000000011641532183 , D:8589934592.00000000000000000000
acca 0.00000000005820766091 , D:17179869184.00000000000000000000
acca 0.00000000002910383046 , D:34359738368.00000000000000000000
acca 0.00000000001455191523 , D:68719476736.00000000000000000000
acca 0.00000000000727595761 , D:137438953472.00000000000000000000
acca 0.00000000000363797881 , D:274877906944.00000000000000000000
acca 0.00000000000181898940 , D:549755813888.00000000000000000000
acca 0.00000000000090949470 , D:1099511627776.00000000000000000000
acca 0.00000000000045474735 , D:2199023255552.00000000000000000000
acca 0.00000000000022737368 , D:4398046511104.00000000000000000000
acca 0.00000000000011368684 , D:8796093022208.00000000000000000000
acca 0.00000000000005684342 , D:17592186044416.00000000000000000000
acca 0.00000000000002842171 , D:35184372088832.00000000000000000000
acca 0.00000000000001421085 , D:70368744177664.00000000000000000000
acca 0.00000000000000710543 , D:140737488355328.00000000000000000000
acca 0.00000000000000355271 , D:281474976710656.00000000000000000000
acca 0.00000000000000177636 , D:562949953421312.00000000000000000000
acca 0.00000000000000088818 , D:1125899906842624.00000000000000000000
acca 0.00000000000000044409 , D:2251799813685248.00000000000000000000
acca 0.00000000000000022204 , D:4503599627370496.00000000000000000000
acca 0.00000000000000011102 , D:9007199254740992.00000000000000000000
acca 0.00000000000000005551 , D:18014398509481984.00000000000000000000
acca 0.00000000000000002776 , D:36028797018963968.00000000000000000000
acca 0.00000000000000001388 , D:72057594037927936.00000000000000000000
acca 0.00000000000000000694 , D:144115188075855872.00000000000000000000
acca 0.00000000000000000347 , D:288230376151711744.00000000000000000000
acca 0.00000000000000000173 , D:576460752303423488.00000000000000000000
acca 0.00000000000000000087 , D:1152921504606846976.00000000000000000000
acca 0.00000000000000000043 , D:2305843009213693952.00000000000000000000
acca 0.00000000000000000022 , D:4611686018427387904.00000000000000000000
acca 0.00000000000000000011 , D:0.00000000000000000000
acca 0.00000000000000000005 , D:0.00000000000000000000

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *