Functional TDD: A Clash of Cultures

42 views
Skip to first unread message

TheViki

unread,
Sep 27, 2012, 7:56:24 AM9/27/12
to sug...@googlegroups.com
Ciao,

visto che l'argomento TDD ha scaldato (a mio parere inutilmente) gli animi nel gruppo.

Rilancio con queste riflessioni di Kent Beck


Allego testo integrale per chi non usa facebook:


I have been taking Bryan O'Sullivan's excellent Haskell course and noticed something during the homework: TDD wasn't working for me, at least not as I apply it in object languages. This has forced me to take a step back and rethink what is really essential about TDD and what is an artifact of the languages I have been using.

 

A warning: I'm only on week two of the course. I did quite a bit of functional programming in the Dinosaur Computing Era, so the style is familiar, but everything contained herein is subject to change. I'm mostly just thinking aloud (and hoping for contributed wisdom.)

 

When I use TDD with an object language like Smalltalk, Java, or PHP I typically start programming with a trivial case--a List with no elements, for example. Working through it encourages me (sounds better than "forces") to think about the metaphors I am using and style of the API. From there I move on to tests that encourage me to write the logic at the heart of the new code. When I can't think of any more tests that could fail, I'm done.

 

In Haskell, though, starting with the null case seems actively misleading. The romance of Haskell seems to come when composing solutions to smaller problems results in a solution to a larger problem. Handling the null case first gets me off track. Thinking about case analysis leads me to decomposition at the exact moment I should be thinking about composition.

 

At the same time there is TDD baby I don't want flying out the window with the object-oriented bathwater. I love (am addicted to, really) the feeling of confidence that comes when a comprehensive suite of tests passes. I like having a stream of bite-sized problems to solve (some day I'll write about using this as a treatment for depression). How can I retain what I like and discard what no longer fits in a Haskell world?

 

TDD can be described operationally as red -> green -> refactor. Within that structure there is subtle wisdom about which tests to write and which to skip, what order to write tests, and when and how much to refactor. My current confusion has led me to identify a handful of principles that underlie TDD and that I don't want to lose, however I end up programming:

  • Double checking. If I think through a problem two different ways and arrive at the same answer, it's much more likely the right answer than if I only think one way. I need to keep some form of double checking. (Haskellians have assured me that type checking is sufficient double checking. I've already written programs that type check just fine and produce wrong answers, so pencil me in as skeptical.)
  • Solution decomposition. I need to be able to work on a part of a problem at a time without having to hold the whole thing in my head at once. Having solved part of a problem I need proof that it is and remains solved.
  • Automatic verification. I need the computer to check whether results are correct. Green should mean, as closely as I can approximate, "Ready to serve nearly a billion users."
  • Outside in. The program's externally visible behavior is more important than its internal structure (leaky abstrations notwithstanding). I want to start my search for a solution from the outside most of the time.

One of the great things about experiencing a new culture is rethinking assumptions. I remember my first week in Switzerland reading an FT article about how Americans think it's rude to stereotype based on national origin but that Europeans use stereotypes as a useful approximation. "Rude!" I thought, "It's just WRONG!" Then I realized I wasn't in Kansas any more and it was time to figure what was really important to me and what was just the result of my lack of exposure. I feel a bit of the same dislocation now. I'll keep you updated. If you have any advice from the other side, I'd appreciate hearing it.

Lorenzo Bolzani

unread,
Sep 30, 2012, 7:38:17 AM9/30/12
to sug...@googlegroups.com

2012/9/27 TheViki <vittorio...@gmail.com>
Ciao,

visto che l'argomento TDD ha scaldato (a mio parere inutilmente) gli animi nel gruppo.

Rilancio con queste riflessioni di Kent Beck



Ho letto e in buona parte condivido quello che scrive Beck. Quello che mi ha stupito e' che la frase "Haskellians have assured me that type checking is sufficient double checking" abbia dato origine a quasi tutti i commenti che seguono.

Da programmatore " a oggetti" capisco come definire tipi specifici (Query piuttosto che String) elimini tutta una serie di problemi, ma molti altri rimangono (anche Java alla fine e' tipizzato staticamente in modo, credo, paragonabile).

Invece, seguendo i commenti, pare che la questione dei tipi in Haskell sia relamente la soluzione di (quasi) tutti i mali o anche solo una buona alternativa agli unit test.

La mia ipotesi e' che essendoci in haskell molti meno problemi legati alla gestione dello stato e side effect l'unica fonte di casini che rimane siano realmente i tipi. Puo' essere?

Pero' di fatto anche le funzioni hanno un loro "stato" cioe' la loro stessa definizione, il modo in cui sono state composte.

In che modo il controllo sui tipi mi assicura che per il Cliente xyz che ha fatto piu' di 100 acquisti l'anno io abbi effettivamente composto la giusta funzione di policy per la gestione dei reclami? Come posso avere una definizione sufficientemente stretta da escludere composizioni sbagliate ma senza perdermi quello che in java sarebbe il polimorfismo?


Immagino che molte delle stesse considerazioni si possano applicare anche a Scala, usato in maniera funzionale.


Ciao

Lorenzo



Mario Fusco

unread,
Sep 30, 2012, 1:57:05 PM9/30/12
to sug...@googlegroups.com
Ho letto e in buona parte condivido quello che scrive Beck. Quello che mi ha stupito e' che la frase "Haskellians have assured me that type checking is sufficient double checking" abbia dato origine a quasi tutti i commenti che seguono.

Anche io l'ho letto (grazie per il link) ed anche io lo condivido in buona parte, eccetto dove dice:


"I love (am addicted to, really) the feeling of confidence that comes when a comprehensive suite of tests passes"

ma questo credo si sia intuito dalle mie precedenti mail :)
 
Da programmatore " a oggetti" capisco come definire tipi specifici (Query piuttosto che String) elimini tutta una serie di problemi, ma molti altri rimangono (anche Java alla fine e' tipizzato staticamente in modo, credo, paragonabile).

La potenza della tipizzazione di Java non è in alcun modo paragonabile a quella di Scala o ancora meno di Haskell anche in considerazione del fatto che il sistema di tipi degli ultimi 2 è Turing completo.

Invece, seguendo i commenti, pare che la questione dei tipi in Haskell sia relamente la soluzione di (quasi) tutti i mali o anche solo una buona alternativa agli unit test.

Provo a giustificare questa affermazione utilizzando un'esempio preso dal primo capitolo di "Scala in Depth". Implementiamo la semplice storia: "un gatto cattura un uccello e lo mangia" con 2 diversi paradigmi, ad oggetti

class Bird
class Cat {
  def catch(b: Bird): Unit = ...
  def eat(): Unit = ...
}
val cat = new Cat
val bird = new Bird
cat.catch(bird)
cat.eat()

e funzionale:

trait Cat
trait Bird
trait Catch
trait FullTummy
def catch(hunter: Cat, prey: Bird): Cat with Catch
def eat(consumer: Cat with Catch): Cat with FullTummy
val story = (catch _) andThen (eat _)
story(new Cat, new Bird)

L'autore in realtà utilizza questo esempio ad uno scopo totalmente diverso: sottolinare il dualismo OOP ed FP in Scala ed evidenziare come nel primo caso la storia si basa principalmente sui nomi (le classi Cat e Bird) mentre nel secondo si sviluppa attorno ai verbi (le funzioni catch e eat).

Credo però che il medesimo esempio possa essere usato anche per giustificare l'asserzione che un sistema di tipi potente come quelli di Haskell o Scala possa essere impiegato per rimpiazzare parte dei test. Come puoi notare lo stato del gatto (quello di avere una preda o quello di essere sazio) nella versione funzionale è implementato attraverso il sistema dei tipi con un mixin (cosa impossibile in Java). In altre parole non hai bisogno di testare che il gatto passato alla funzione eat abbia una preda e quello ritornato dalla funzione stessa sia sazio: ciò è garantito dal sistema di tipi e il codice nemmeno compilerebbe se non fosse così.

Ovviamente ciò non è sufficiente (a mio parere) ad esimerti dall'avere una suite di test quanto più possibile esaustiva, però spero serva a giustificare che hai bisogno di avere meno test meno per raggiungere tale obiettivo.

Mario

Ivano Pagano

unread,
Oct 1, 2012, 11:00:10 AM10/1/12
to sug...@googlegroups.com
Io condivido la conclusione di Mario e aggiungo una mia personale interpretazione del commento di Beck sulle garanzie di un linguaggio fortemente tipizzato e con un ottimo motore inferenziale, come haskell.

Sembrerebbe che gli haskellisti abbiano la convinzione che la stragrande maggioranza dei bug e degli errori e' legata ad un erroneo utilizzo dei tipi. Su questo bisognerebbe proprio capire di che tipo di programmatori parliamo...
Personalmente sono convinto che un type system avanzato puo' essere un grande aiuto: sono a favore di Options, Phantom Types e quant'altro per rendere piu' esplicito l'uso e il significato di una funzione.
D'altra parte il bacino di utenza di Java, e quindi di Scala, e' molto piu' esteso di quello di haskell, pertanto anche la competenza media dei programmatori non sara' la stessa.
Di base ho l'impressione che i programmatori di haskell hanno una maggiore preparazione formale oltre ad essere piu' capaci a programmare ad un livello maggiore di sofisticazione e astrazione.
Nel momento in cui scala dovesse prendere piede, non darei per scontato che il contenuto di una funzione possa essere considerato sostanzialmente bug-free.

Il type system mi dice cosa entra e cosa esce, ma non implica altro:

def concatLists[A](a: List[A], b: List[A]): List[A] = Nil

e il compilatore e' tranquillo e contento...
suppongo che ad occhio riusciamo tutti a capire invece che qualcosa non va...

Quindi mi viene da concludere che siamo lontani dal non aver bisogno di un TDD.


p.s. oltre a questo, il TDD e' anche un metodo per focalizzarsi sull'essenziale senza perdere tempo con generalizzazioni che non saranno mai utilizzate, e questo vale in qualunque linguaggio.
Il test mi dice cosa ho bisogno di ottenere, ed e' quindi una buona guida per lo sviluppo (fermo restando che il codice dovrebbe essere *ben progettato* e non focalizzato _solo_ a far passare i test)

A presto,
Ivano

On Sunday, September 30, 2012 7:57:06 PM UTC+2, Mario Fusco wrote:
[...]

Lorenzo Bolzani

unread,
Oct 1, 2012, 3:00:20 PM10/1/12
to sug...@googlegroups.com
Il giorno 30 settembre 2012 19:57, Mario Fusco <mario...@gmail.com> ha scritto:
 
Provo a giustificare questa affermazione utilizzando un'esempio preso dal primo capitolo di "Scala in Depth". Implementiamo la semplice storia: "un gatto cattura un uccello e lo mangia" con 2 diversi paradigmi, ad oggetti
 [...]
Credo però che il medesimo esempio possa essere usato anche per giustificare l'asserzione che un sistema di tipi potente come quelli di Haskell o Scala possa essere impiegato per rimpiazzare parte dei test. Come puoi notare lo stato del gatto (quello di avere una preda o quello di essere sazio) nella versione funzionale è implementato attraverso il sistema dei tipi con un mixin (cosa impossibile in Java). In altre parole non hai bisogno di testare che il gatto passato alla funzione eat abbia una preda e quello ritornato dalla funzione stessa sia sazio: ciò è garantito dal sistema di tipi e il codice nemmeno compilerebbe se non fosse così.

Finalmente un esempio chiaro, grazie, adesso mi torna di piu'. Le variazioni di stato le trasformo in variazioni sul tipo in modo da "agganciare" il sistema di tipi e sfruttare quello.
Mi rimangono un paio di dubbi:

1. quando poi prendo tutti i gatti e vado a metterli in una lista e li processo in quanto "Cat" perdo i miei rifinitissimi tipi e sono fregato. Immagino che qualche meccanismo di double/multiple dispatch del linguaggio mi permetta di recuperarli e portarli avanti comunque.

2. Supponiamo di avere un oggetto Query che ha una lista di campi da estrarre. Visto che la lista non deve essere vuota la definisco come NonEmptyList[Column] cosi' ho la certezza che non possano essere create Query incomplete.
Il dubbio e': che cosa mi garantisce che siano stati usati tutti i tipi corretti in tutti i punti? Se avessi usato List[Column] o persino List[Object] avrebbe compilato comunque.
Probabilmente e' un po' come il private o i tipi in java: se lo metto e provo a violarlo il compilatore me lo segnala. Se me lo sono dimenticato, bhe, cavoli miei.

Ovviamente tutto deve essere immutabile, altrimenti potrei togliere la preda al mio gatto senza che questo cambi tipo.

Per capirlo davvero servirebbe vedere in azione tutti i pezzi del puzzle: tipi specifici, immutabilita', inferenza del compilatore, ecc. Presi singolarmente probabilmente rendono meno l'idea.


Ciao

Lorenzo





Ivano Pagano

unread,
Oct 2, 2012, 10:27:25 AM10/2/12
to sug...@googlegroups.com


On Monday, October 1, 2012 9:00:22 PM UTC+2, lorenzo wrote:
1. quando poi prendo tutti i gatti e vado a metterli in una lista e li processo in quanto "Cat" perdo i miei rifinitissimi tipi e sono fregato. Immagino che qualche meccanismo di double/multiple dispatch del linguaggio mi permetta di recuperarli e portarli avanti comunque.


val cats: List[Cat] = List(tom, rumtumtugger, felix, topcat)
val smartCats = cats.filter(_.isInstanceOf[FullTummy])

Putroppo la seconda lista non ha il type parameter giusto, ma puoi sempre chiamare  asInstanceOf[FullTummy]
 
2. Supponiamo di avere un oggetto Query che ha una lista di campi da estrarre. Visto che la lista non deve essere vuota la definisco come NonEmptyList[Column] cosi' ho la certezza che non possano essere create Query incomplete.
Il dubbio e': che cosa mi garantisce che siano stati usati tutti i tipi corretti in tutti i punti? Se avessi usato List[Column] o persino List[Object] avrebbe compilato comunque.
Probabilmente e' un po' come il private o i tipi in java: se lo metto e provo a violarlo il compilatore me lo segnala. Se me lo sono dimenticato, bhe, cavoli miei.


A prima vista direi che concordo, il type system e' li' se lo vuoi usare, ma se non lo sfrutti, non puo' garantirti nulla...

 
Ovviamente tutto deve essere immutabile, altrimenti potrei togliere la preda al mio gatto senza che questo cambi tipo.


Veramente dipende dall'implementazione... l'esempio di Mario funziona cosi' come descritto (piu' o meno) anche senza definire nemmeno un attributo nel trait Cat.
Non c'e' nulla da togliere al gatto!

Le potenzialita' del type system sono tali da poter funzionare per dimostrare la consistenza delle tue operazioni anche senza definire altro che i tipi e qualche funzione su di essi.

In alternativa puoi fare override nei tipi specifici per impedire le operazioni che non hanno senso, in modo da non poter mai togliere la preda ad un gatto che ha gia' mangiato... ma tutto questo e' il solito discorso del corretto incapsulamento dei dati.
 

Ciao

Lorenzo


Ciao
Ivano 

Dale Wijnand

unread,
Oct 2, 2012, 10:32:20 AM10/2/12
to sug...@googlegroups.com
2012/10/2 Ivano Pagano <ivano....@gmail.com>
On Monday, October 1, 2012 9:00:22 PM UTC+2, lorenzo wrote:
1. quando poi prendo tutti i gatti e vado a metterli in una lista e li processo in quanto "Cat" perdo i miei rifinitissimi tipi e sono fregato. Immagino che qualche meccanismo di double/multiple dispatch del linguaggio mi permetta di recuperarli e portarli avanti comunque.


val cats: List[Cat] = List(tom, rumtumtugger, felix, topcat)
val smartCats = cats.filter(_.isInstanceOf[FullTummy])

Putroppo la seconda lista non ha il type parameter giusto, ma puoi sempre chiamare  asInstanceOf[FullTummy]

val smartCats = cats.collect { case smartCat: FullTummy => smartCat }

Dale 

Ivano Pagano

unread,
Oct 2, 2012, 12:47:01 PM10/2/12
to sug...@googlegroups.com
Grazie Dale!
Immaginavo che ci fosse un modo "smart" (scusate il gioco di parole).

Vorrei solo commentare il fatto che, come dimostrano questi esempi, ci sono enormi potenzialita' in scala per lavorare in modo semplice, chiaro ed esatto, anche solo sfruttando il type system e la potenza delle librerie standard.

Ivano

Lorenzo Bolzani

unread,
Oct 2, 2012, 1:57:06 PM10/2/12
to sug...@googlegroups.com
Il giorno 02 ottobre 2012 16:32, Dale Wijnand <dale.w...@gmail.com> ha scritto:

val cats: List[Cat] = List(tom, rumtumtugger, felix, topcat)
val smartCats = cats.filter(_.isInstanceOf[FullTummy])

Putroppo la seconda lista non ha il type parameter giusto, ma puoi sempre chiamare  asInstanceOf[FullTummy]

val smartCats = cats.collect { case smartCat: FullTummy => smartCat }


Si', cosi' funziona, ma avevo in mente qualcosa di diverso. Un meccanismo fornito direttamente dal linguaggio, in modo da non dover reintrodurre continuamente i tipi con degli "pseudo-cast" ma dove i tipi siano gia' noti al compilatore o almeno recuperabili a runtime.

Se ogni operazione che posso fare e' associata ad un tipo specifico ed ho quindi molti tipi specifici che descrivono il mio dominio e i suoi differenti stati allora o sono costretto a ripetere il codice qui sopra centinaia di volte, o il linguaggio mi aiuta oppure e' una pessima idea mettere i Cat tutti assieme (Cat di per se' avrebbe pochissimi metodi).
Immagino ci siano una serie di "pattern funzionali" e di meccanismi del linguaggio (pattern matching, multiple dispatch, ecc.) nati apposta per automatizzare/nascondere il ciclo for qui sopra.


Ciao

Lorenzo



Dale Wijnand

unread,
Oct 2, 2012, 2:32:53 PM10/2/12
to sug...@googlegroups.com
2012/10/2 Lorenzo Bolzani <l.bo...@gmail.com>
Quando dici 'i tipi siano gia' noti al compilatore' mi fai pensare ad una cosa importantissima che esiste in scala: sealed types e exhaustive pattern matching.


Un'altra cosa che esiste anche che magari ti potrebbe interessare sono i type classes: http://www.sidewayscoding.com/2011/01/introduction-to-type-classes-in-scala.html

Dale
 
Reply all
Reply to author
Forward
0 new messages