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:
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.
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.
[...]
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ì.
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.
Ciao
Lorenzo
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 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 }