Wednesday, December 15, 2010

codebreaker in javascript + underscore.js

Prosegue la versione in più linguaggi del codebreaker kata.
Ora è la volta di una versione in javascript, che utilizza come ambiente jspec e rhino., e usa la libreria underscore.js
Tali ambienti sono installati in una macchina virtuale Ubuntu.
Per i primi 10 secondi c'è un po' di blink dello schermo, e la musica di sottofondo parte dopo qualche minuto.
Lo screenacast include la parte di inizializzazione del progetto, per i primi due minuti e mezzo, il tempo netto di esecuzione è poco più di 10 minuti.

La sequenza dei test è la seguente:
(nota: tutte le funzioni che iniziano con _. fanno parte della libreria "underscore.js")

time, guess secret: [commento] -> answer

3:00 - "xxxx" "rgby": nessun match -> ""

3:25 - "yxxx" "rgby": un match imperfetto (prima posizione)-> "n" (risolto con un if)

3:55 - "xyxx" "rgby": un match imperfetto (seconda posizione) -> "n" (risolto con un or aggiunto all'if precedente)

4:10 - . refactoring -> i due or dell'if precedente vengono trasformati in un ciclo for su tutti gli elementi del guess (e questo significa che il codice sarà in grado di gestire anche i singoli match imperfetti in terza e quarta posizione, anche se non viene scritto nessun ulteriore test di questo tipo).

4:35 - "byxx" "rgby" : due match imperfetti -> nel ciclo for, in caso di match, si accumula il risultato da restituire (concatenazione di 'm'), anziché uscire immediatamente. Questo intuitivamente risolve altri casi di match imperfetti (di diverso numero e diverse posizioni) e quindi non vengono aggiunti altri casi di test simili.

0:51 .refactoring -> il ciclo for viene rimpiazzato dal costrutto _.each

5:30 "yyxx" "rgby" -> un solo match "m" anziché due, perché la 'y' occorre una volta nel secret. Soluzione: ogni volta che viene contato un match, il relativo colore viene rimosso dal secret stesso, così non verrà più contato. (nota: non è così banale "dedurre" questo "requisito" imo).

6:30 .refactoring: viene estratto il metodo che restituisce, da una stringa, la stessa senza uno specifico carattere (nota: lì introduco un bug perché aggiungo la chiamata al nuovo metodo senza cancellare il codice che esegue la rimozione, quindi la rimozione viene fatta due volte: il bug viene scoperto e corretto più avanti.

7:22 .refactoring -> trasformo la chiamata _.each() nella chiamata _.reduce(). Questo consente di eliminare l'uso di una variabile di accumulazione del risultato (toReturn) e di tagliare due righe. (nota avrei dovuto accorgermi del baco descritto sopra perché la riga in questione ha una lunghezza sospetta)

8:02 .refactoring -> l'intenzione è di separare la logica del conteggio del numero dei match dal fatto di volerlo rappresentare in una stringa di 'm'. Dal metodo mark così creato, ne creo uno nuovo (countNonPositionals), che anziché accumulare in una stringa tanti 'm' quanti sono i match non positionali, li accumula in un numero (a questo punto lui non sa come sarà presentato il risultato)

8:16 .refactoring -> eseguo una combinazione di _.reduce e _.range applicata alla funzione countPositionals per ottenere una concatenazione di m pari al numero di match non positionali (e questo sostituisce l'attuale funzionalità della "mark")

8:51 .refactoring -> introduco un metodo apposito che trasforma un numero in una sequenza di caratteri

10:00 "rxxx" "rgby" : match perfetto in prima posizione -> duplico i metodi mark e countNonPositionals in positionalMatch e countPositionals. Usano la stessa logica, salvo accumulare le 'p' al posto delle 'm' e usare la logica di confronto posizione per posizione, anziché la logica del "contiene". Viene anche tolta la chiamata alla rimozione dei caratteri che matchano (comincio ad accorgermi che qualcosa non va, a riguardo della logica di rimozione duplicata, ma non metto ancora a posto il bug ereditato nel punto originario, cioè nella countNonPositionals).

11:30 refactoring del test (o correzione di errore nel test stesso) -> devo chiamare la positionalMatch anziché la mark (che solo dopo unificherà entrambe le logiche).

11:36 nuovi test: match posizionale in seconda posizione e doppio match posizionale (non cambio nulla del codice perché già passa questi test)

12:13 il test che chiama la logica del match posizionale, ora chiama la mark stessa (che fin'ora contava solo i match non posizionali). -> inizio a concatenare le logiche delle due chiamata (non chiamo esplicitamente la positionalMatch perché in teoria è destinata a scomparire), mi accorgo che anziché restituire "p" viene restituito "pm" e presumo che il non posizionale "intruso" 'm' sia un riconteggio come "match non posizionle" di quello precedente. Posso risolvere elimino dal conteggio dei non posizionali, il numero dei posizionali. Lo faccio clonando la countNonPositionals in countAnyMatch, e poi trasformando la countNonPositionals in modo che restituisca il valore della chiamata a countAnyMatch cui viene sottratto il risultato della chiamata a countPositionals (togliendo cioè il falsi non posizionali).
Da lì in poi ci sono vari casi di test significativi che passano, dal che si comincia a pensare che siamo arrivati all'algoritmo giusto, salvo la correzione del bug, come da punto seguente:

14:45 "rryy" "yyrr" -> mi aspetto quattro match imperfetti, ne vengono fuori solo due a causa del bug della doppia rimozione in caso di match nella countAnyMatch (ex countNonPositionals). Dopo un attimo di incertezza elimino il bug.

Conclusioni: è un po' più complicato della versione originaria in C#, ma forse il risultato finale è più espressivo, almeno per quanto riguarda la funzione "mark".
È quasi completamente stateless, e l'uso della libreria _. avvicina la soluzione a quello che si farebbe tipicamente con linguaggi funzionali o estensioni come LinQ.

In teoria non è previsto che in un "kata" con un bug introdotto e poi corretto, ma è comunque interessante.

Nel libro di Rspec lo stesso problema viene trattato in vari modi. Sarebbe da riguardare per qualche confronto.

Ciao.

No comments: