13.12.2008

Dojo 60 - Snap em Haskell

Postado às 00:32 por

 O Dojo iniciou com a votação do problema e da linguagem de programação. As linguagens sugeridas foram Haskell, Ruby e Java. A idéia de fazer o Dojo em java seria respeitando umas regras que o pessoal da Thoughtworks montou: 

 http://binstock.blogspot.com/2008/04/perfecting-oos-small-classes-and....

Essas 9 regras são exercícios para aperfeiçoar a escrita de boas implementações de Orientação a Objetos. São elas:

  1. Usar somente um nível de identação por método.
  2. Não usar a palavra reservada ‘else’.
  3. Envolver os tipos primitivos e Strings.
  4. Usar somene 1 ponto por linha de código.
  5. Não abreviar nomes.
  6. Manter entidades pequenas.
  7. Não usar mais que duas variáveis de instância na classe.
  8. Usar classes de coleções que sejam as mais genéricas.
  9. Não usar getters e setters.

Porém o problema escolhido por votação foi o Snap e a linguagem Haskell.

Problema

O problema Snap se trata de clicks (definidos como pontos) e desenhos de vértices. Sendo que se o click estiver em uma área de atração de um determinado ponto, a posição do click será a própria posição do ponto de atração. O site desse problema está detalhado aqui.

 Codando

Teve uma novidade da rotação das duplas. Após os 7 minutos, entrava uma pessoa direto na programação e  quem estava codando passaria a ser o “co-piloto”.

Para a resolução do problema, foram criados dois arquivos: Tests.hs (para os testes) e Snap.hs (implementação do problema). Bem ao estilo TDD, a classe de teste começou com as linhas:

Tests.hs module Main where import Test.HUnit import Snap

main = runTestTT testes

testes = TestList [testeCalculaDistancias]

testeCalculaDistancias = TestList[

"Distancia entre um ponto e ele mesmo deveria ser 0" ~:
distancia (Ponto 0 0) (Ponto 0 0 ) ~?= 0
    ]

Esse código simplesmente significa que o teste vai assegurar que a distância entre o Ponto na posição x = 0 e y = 0, e outro Ponto na posição x = 0 e y = 0, a distância é zero (Linha 11). É claro, pois eles estão na mesma posição ! :) Disparando esse teste, vão ocorrer erros, pois não existe a definição de "Ponto" e "distância". Portanto vamos implementá-los.

Snap.hs module Snap where

data Ponto = Ponto Int Int

distancia :: Ponto -> Ponto -> Int distancia _ _ = 0

Na linha 3 é definido o Ponto, que será representado por 'Ponto' seguido de dois valores numéricos (Int Int). Na linha 5 é definido a função 'distância' que recebe dois pontos por parâmetro. Na linha 6 diz que distância, que recebe qualquer coisa no primeiro parâmetro e qualquer coisa no segundo parâmetro, retorna 0 (zero). Agora o teste passa com sucesso!

Porém ainda não está correto dizer que a distância entre qualquer ponto é zero. Então vamos escrever mais testes.

Tests.hs  "Distancia entre o ponto 0 0 e 0 1 deveria ser 1" ~: distancia (Ponto 0 0) (Ponto 0 1 ) ~?= 1

Incluindo essas duas linhas, agora o teste falha. A distância entre o ponto 0 0 e o ponto 0 1 é 1, porém o método 'distancia' sempre retorna zero. Vamos alterar a implementação desse método:

Snap.hs

distancia :: Ponto -> Ponto -> Int distancia (Ponto 0 0) (Ponto 0 1 ) = 1 distancia (Ponto 0 0) (Ponto 0 2 ) = 2 distancia _ _ = 0

As linhas 2 e 3 foram incluídas e agora o teste passa! Porém, apesar de estarmos roubando para o teste passar, esses passos pequenos são muito importantes. São chamados de baby steps. Baby step é uma técnica de metodologias ágeis que facilita a simplicidade do código, pois só precisamos desenvolver o que é realmente necessário para passar no teste.

O ciclo do TDD é: 

Portanto, agora que os testes estão passando, vamos refatorar para o código ficar menos hard coded. As linhas:

Snap.hs distancia (Ponto 0 0) (Ponto 0 1 ) = 1 distancia (Ponto 0 0) (Ponto 0 2 ) = 2 distancia _ _ = 0

Podem agora ser substituídas por: distancia (Ponto 0 a) (Ponto 0 b ) = b-a

Agora nossa regra está funcionando para vértices na vertical. Porém ainda não está pronto. Vamos escrever mais um teste:

Tests.hs "Distancia entre o ponto 0 0 e 1 0 deveria ser 1" ~: distancia (Ponto 0 0) (Ponto 1 0 ) ~?= 1

Está passando 2 testes mas esse último falha. Vamos à implementação para esse teste incluído passar:

Snap.hs distancia :: Ponto -> Ponto -> Int distancia (Ponto 0 a) (Ponto 0 b ) = b-a distancia (Ponto a 0) (Ponto b 0 ) = b-a

Antes só passavam os testes para vértices na vertical. Agora está passando para vértices na horizontal também. Vamos fazer um teste e ver se passa na diagonal. Incluindo isso no teste ficaria:

Tests.hs

"Distancia entre o ponto 1 0 e 1 1 deveria ser 1" ~: distancia (Ponto 1 0) (Ponto 1 1 ) ~?= 1

A implementação para passar nesse teste, ainda roubando, ficaria assim:

Snap.hs

distancia (Ponto 1 0) (Ponto 1 1 ) = 1

Essa implementação está hard coded, mas é importante que seja dessa maneira pois precisamos manter o foco em ficar verde no teste, e aí sim fazer a refatoração. Ok, como os testes estão passando novamente e está tudo no verde, vamos refatorar. As linhas:

distancia (Ponto 0 a) (Ponto 0 b ) = b-a distancia (Ponto a 0) (Ponto b 0 ) = b-a distancia (Ponto 1 0) (Ponto 1 1 ) = 1

Porem ser substituídas pela linha:

distancia (Ponto x1 y1) (Ponto x2 y2 ) = x2-x1 + y2-y1

Tudo passando! Vamos escrever mais testes para ver se a regra está certa:

Tests.hs "Distancia entre o ponto 0 3 e 4 0 deveria ser 5 distancia (Ponto 0 3) (Ponto 4 0 ) ~?= 5

Com esse teste o resultado falha, portanto a fórmula acima ainda não está pronta. O correto seria implementar o teorema de pitágoras para calcular a distância entre os pontos:

Snap.hs

data Ponto = Ponto Float Float

distancia :: Ponto -> Ponto -> Float distancia (Ponto x1 y1) (Ponto x2 y2 ) = sqrt ((x2-x1)(x2-x1) + (y2-y1)(y2-y1))

Concluindo

Vamos parar por aqui. O problema ainda não está resolvido, mas mostrei esses passos para percebermos a importância do baby step e como a modelagem pode ficar melhor e com mais qualidade. Para ver o que foi codado, entre no site do Github aqui.

O TDD nos direciona para uma maior simplicidade para ser desenvolvido somente o necessário para passar no teste. Além de fornecer mais confiança para devidas refatorações.

Retrospectiva

No final teve, como de costume, a nossa retrospectiva e os pontos a melhorar foram:

• "Não precisa do b-a", sobre um dos baby-steps que poderia ser desnecessário • "Nano baby step => seguro mas muito lento". Sobre passos muito pequenos. • Alguns passos grandes/rápidos em alguns momentos. • Faltaram mais explicações sobre o que é baby-step durante o código • "Faltou tão pouco" para terminar o problema. Apesar que esse não é o objetivo final do Dojo.+ • Teve gente que não gostou desse esquema de rotação invertido. • Projetor estava ruim. • Baby-step? Não "grown-up wannabe steps". • Perdemos muito tempo criando templates do projeto. • Emacs é melhor que Gedit para codar Haskell

Os pontos positivos foram:

• Tipos de dados no Haskell • Haskell • Pessoas gostaram no rodízio invertido. • Problema legal, simples e bom para TDD • Baby steps • " Ëœ é sempre antes."  Sobre a notação no Haskell.

Assuntos em Parking lote foram

• TODOs ajudam ou atrapalham? • Geradores do ruby são ótimos para gerar templates