Ancora su documentazione e tests

di Uriel Fanelli

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 sintassi non 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 limiti concettuali 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 ALCUNI avranno 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

di Uriel Fanelli

PS: questo pezzo e le altre magnifiche elargizioni di saggezza di Uriel sono analizzate su un altro blog degli Untermenschen che curano questo specchio.
PPS: pezzo automagicamente caricato da Fornello!

Annunci

15 pensieri su “Ancora su documentazione e tests

  1. Hint 1: warning check. Se passi a perl un database da 45 giga, metti una vviso (sopra ai 100 mega? boh?) che dice 2occhio potrebbe andar lento, vuoi continuare? Questo per l’utent emedio. Un programmatore (uno serio, non un piadinaro) sa come si comporta perl e non gli passa godzilla come argomento.

    Hint 2: per chi è il programmino derivata?
    A) per un utente a caso che sa vagamente cosa sia una derivata ma vuole un risultato: metti un controllo di continuità/derivabilità.
    B) per uno “del ramo”: metti un warning per cui il controllo lo devi fare tu.

    E’ ovvio che se fai il programmino TeorPitagora e poi lo applichi a un pentagono, non funziona. Ma se usi funzioni a caso sperando che esca il risultato giusto, hai voglia ad aspettare.

    Mi piace

    • E’ ovvio che il problema non è dovuto al comportamento di map o grep, ben documentato in qualunque decente introduzione a Perl, ma alla memorizzazione in un array del contenuto di un file di log da 200MB, che poi per giunta viene copiato in tutto o in parte. La descrizione è parecchio fuorviante.

      Mi piace

      • Ci sarebbero degli angli incompetenti che dicono di aver inventato una tecnica che fonde codice, documentazione e spiegazione. Mi pare si chiami Literate Programming. Se non erro se l’é inventata uno sconosciuto accademico di nome Knuth. Ah, è proprio vero che gli universitari son subumani… ☺

        Mi piace

  2. Alla fine che ha scritto di male? È la tradizionale sequela di paurose coglionate che escono da chiunque cerchi di programmare senza aver studiato teoria delle macchine calcolatrici. Che, di questi tempi, è la maggioranza,

    Mi piace

  3. A parte che la documentazione ufficiale di linguaggi/librerie è quasi sempre molto più dettagliata di quanto vuole far credere, mi ricordo che qualcuno diceva che chi non riesce a reperire su Internet le informazioni che gli servono da solo è statisticamente morto. Aha. Era proprio Uriel.
    È abbastanza divertente comunque notare come questo polpettone arrivi a seguito di un post dove oltre a questo mantra de “il codice NON è documentazione” ripeteva anche che un linguaggio di programmazione serio dovrebbe permettere a due programmatori che non comunicano tra loro di collaborare ugualmente. Il perchè di questa cosa non me la spiego, forse perchè è il primo che si rompe le palle di commentare il codice e scrivere documentazione, e tantomeno vuole rivolgere la parola ai colleghi.
    Ancora più simpatico l’episodio che ha scatenato il primo post appunto, come al solito un piccolo fatto non rappresentativo della realtà viene ingigantito e preso come base per un discorso universale.
    Questa volta è successo che alcuni programmatori hanno realizzato un software di sincronizzazione file basato su un protocollo P2P di loro realizzazione, ed essendo il tutto sotto licenza MIT, è effettivamente possibile per uno sviluppatore terzo prendere solo l’implementazione del protocollo ed utilizzarla come libreria per la propria applicazione. Questo è quello che vuole fare Uriel, e ne ha pieno diritto, ma deve rendersi conto che l’obbiettivo primario degli sviluppatori non era quello di produrre una libreria e dunque non hanno tutto questo tempo da dedicare alla documentazione del protocollo. Anzi, se non altro hanno almeno documentato la specifica del protocollo, ma per il resto, chi voglia usarlo è da solo.
    Così gli viene risposto, e lui si incazza. (https://github.com/syncthing/protocol/issues/11).
    Ricordo che ogni suo post sull’open source scaturiva da situazioni del genere, si trovava spesso ad utilizzare il software di progetti piccoli e mal organizzati, e allora partiva l’invettiva contro la comunità open source, secondo lui responsabile del fallimento di ogni piccolo progetto condotto nei ritagli di tempo da gente che ha di meglio da fare.

    Mi piace

    • E’ troppo ridicolo quando esce dal nido 😀

      Praticamente è stato bannato subito e spernacchiato con l’ultimo messaggio come a dire: “se magari ti prendi la briga di leggere le specifiche come ho fatto io anzichè perdere tempo a lamentarti, capiresti come funziona la libreria” 😀

      Liked by 1 persona

        • Sì, e anche giustamente direi.
          Non è neanche una libreria, ma un componente di una applicazione completa, per cui gli sviluppatori non sono tenuti a redarre una guida per il programmatore, visto che lo scopo primario del protocollo è essere utilizzato nella loro applicazione. Se qualcun altro volesse usarlo, bene, il funzionamento del protocollo è documentato, manca però la documentazione della sua API diciamo.
          Considerando che l’alternativa sarebbe scrivere da zero un protocollo analogo non c’è tanto da lamentarsi.

          Mi piace

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...