Home

24 dias de Hackage, 2015 - dia 21 - hood, GHood, Hoed: Debugging orientado à observação no Haskell

por Franklin Chen

traduzido por Pedro Yamada

Esse é um artigo escrito por Franklin Chen e traduzido para o português. Ler original.

Índice de toda a série

O índice de toda a série está no topo do artigo para o dia 1.

Encontro HaskellBR São Paulo

Vamos nos encontrar em São Paulo em Janeiro de 2016. Marque sua presença e comente se não puder vir.

Vote se o encontro terá workshops, um dojo ou palestras.

Dia 21

(Discussão no Reddit)

Como você debugga seu código Haskell?

Eu tenho que confessar de cara que não tenho uma boa resposta para a questão de como debuggar Haskell.

Não sou a pessoa certa para falar sobre debuggers, porque a última vez que usei uma ferramenta de debugging oficial foi quando estava desenvolvendo C e C++ e usava ferramentas como o gdb e interfaces de mais alto-nível para ele, e desde então, meu processo de debugging para muitas linguagens tem envolvido olhar a stack traces e logs, inserir expressões “print”, escrever testes melhores e refatorar o código para encontrar a causa do problema. Eu não uso mais aplicações de debugging oficiais tanto assim (com breakpoints, stepping, etc.). Mas será que eu deveria?

A questão fica ainda mais complicada quando estamos trabalhando em Haskell, porque eu acho que é honesto dizer que Haskell não tem uma boa história com debugging.

Eu dei uma olhada em uma família de ferramentas de debugging para o haskell, incluindo hood GHood, e Hoed, todos baseados no mesmo conceito: anotações manuais no código fonte, de forma que traces mais úteis possam ser gerados e analisados de formas interessantes.

Hood

Vamos dar uma olhada no hood antes, que significa “Haskell Object Observation Debugger”. É tudo sobre observação por meio da type-class Observable a e uma função de instrumentação observe:

observe :: Observable a => String -> a -> a

Efeitos colaterais secretos!

Atenção! Apesar da sua assinatura de tipo, essa função realiza efeitos por usando o unsafePerformIO, então você deve tomar cuidado quanto a como escrever código que usa o observe, para conseguir os traces que quer.

Exemplo de instrumentação de uma pipeline

Vamos instrumentar a pipeline de contagens de palavras do dia 8 . Nós importamos Debug.Hood.Observe e copiamos e colamos o código original com modificações para inserir chamadas para o observe.

{-# LANGUAGE OverloadedStrings #-}

module HoodExample where

-- | hood
import Debug.Hood.Observe (Observable(..), observe, observeBase, printO)

import qualified Data.MultiSet as MultiSet
import qualified Data.Text.Lazy as LazyText
import qualified Data.Text.Lazy.Builder as LazyBuilder
import Data.Text.Lazy.Builder.Int (decimal)
import Data.Ord (Down(..))
import qualified Data.Char as Char
import qualified Data.List as List
import Control.Arrow ((>>>))
import Data.Monoid ((<>))

-- | Break up text into "words" separated by spaces, while treating
-- non-letters as spaces, count them, output a report line for each
-- word and its count in descending order of count but ascending order
-- of word.
wordCount :: LazyText.Text -> LazyText.Text
wordCount = observe "(1) wordCount"
  >>> LazyText.map replaceNonLetterWithSpace >>> observe "(2) map replaceNonLetterWithSpace"
  >>> LazyText.words             >>> observe "(3) LazyText.words"
  >>> MultiSet.fromList          >>> observe "(4) MultiSet.fromList"
  >>> MultiSet.toOccurList       >>> observe "(5) MultiSet.toOccurList"
  >>> List.sortOn (snd >>> Down) >>> observe "(6) List.sortOn (snd >>> Down)"
  >>> map summarizeWordCount     >>> observe "(7) map summarizeWordCount"
  >>> mconcat                    >>> observe "(8) mconcat"
  >>> LazyBuilder.toLazyText     >>> observe "(9) LazyBuilder.toLazyText"

replaceNonLetterWithSpace :: Char -> Char
replaceNonLetterWithSpace c
  | Char.isLetter c = c
  | otherwise = ' '

summarizeWordCount :: (LazyText.Text, MultiSet.Occur) -> LazyBuilder.Builder
summarizeWordCount (word, count) =
  LazyBuilder.fromLazyText word <> " " <> decimal count <> "\n"

(Peço desculpas pelo boilerplate sem sentido na descrição tipo String de cada ponto de observação: poderíamos escrever um wrapper usando Template Haskell, eu imagino, se desejado)

Aqui eu escolhi só instrumentar os “estágios” naturais dos dados na pipeline. Se quisesse, também podería instrumentar qualquer outro nível, e.g. o resultado de chamar summarizeWordCount (com summarizeWordCount >> observe "summarizeWordCount"), ou muitos outros níveis poderosos, tais como instrumentar uma função, não só o resultado de uma chamada a uma função, e.g. observe "summarizeWordCountfunction" summarizeWordCount, que resulta na coleção de todas as chamadas para a função.

Implementando uma type-class Observable

Irritantemente, porque tudo é baseado na type-class Observable, ignoramos avisos do GHC e criamos instâncias órfãs para várias tipos (a alternativa, adicionando um wrapper newtype em todos os lugares é um pouco trabalhosa nessa situação):

-- | Some orphan instances of 'Observable'.
instance Observable LazyText.Text where
  observer = observeBase
instance (Observable a, Show a) => Observable (MultiSet.MultiSet a) where
  observer = observeBase
instance Observable LazyBuilder.Builder where
  observer = observeBase

Saída de exemplo

Para simplicidade, vamos usar printO :: Show a => a -> IO () para executar de forma que um trace seja impresso. Mais genéricamente, há um runO que executa uma ação IO arbitrária.

exampleRun :: IO ()
exampleRun = printO $
  wordCount "I have all-too-many words; words I don't like much!"

A saída (eu só rodei isso no GHCi):

-- (1) wordCount
  "I have all-too-many words; words I don't like much!"
-- (2) map replaceNonLetterWithSpace
  "I have all too many words  words I don t like much "
-- (3) LazyText.words
   "I" :  "have" :  "all" :  "too" :  "many" :  "words" :  "words" :  "I" :
  "don" :  "t" :  "like" :  "much" : []
-- (4) MultiSet.fromList
  fromOccurList [("I",2),("all",1),("don",1),("have",1),("like",1),("many",1),("much",1),("t",1),("too",1),("words",2)]
-- (5) MultiSet.toOccurList
   ("I", 2) :  ("all", 1) :  ("don", 1) :  ("have", 1) :  ("like", 1) :
  ("many", 1) :  ("much", 1) :  ("t", 1) :  ("too", 1) :  ("words", 2) : []
-- (6) List.sortOn (snd >>> Down)
   ("I", 2) :  ("words", 2) :  ("all", 1) :  ("don", 1) :  ("have", 1) :
  ("like", 1) :  ("many", 1) :  ("much", 1) :  ("t", 1) :  ("too", 1) : []
-- (7) map summarizeWordCount
   "I 2\n" :  "words 2\n" :  "all 1\n" :  "don 1\n" :  "have 1\n" :
  "like 1\n" :  "many 1\n" :  "much 1\n" :  "t 1\n" :  "too 1\n" : []
-- (8) mconcat
  "I 2\nwords 2\nall 1\ndon 1\nhave 1\nlike 1\nmany 1\nmuch 1\nt 1\ntoo 1\n"
-- (9) LazyBuilder.toLazyText
  "I 2\nwords 2\nall 1\ndon 1\nhave 1\nlike 1\nmany 1\nmuch 1\nt 1\ntoo 1\n"

Isso definitivamente pode ser uma forma útil de debuggar pipelines, ou só gerar traces para fins educacionais. Por exemplo, aqui nós podemos ver imediatamente que não recebemos a resposta que queríamos (classificar “don’t” como uma palavra) porque no terceiro estágio, nós recebemos “don”, o que significa que algo deu errado nos estágios anteriores. No caso só havia um outro, a remoção de caracteres que não fizessem parte de palavras - como o '.

Há muito mais que podemos fazer com o hood, mas isso dá um gostinho.

GHood

Agora, nós mudamos o olhar brevemente para o GHood, que tem a mesma interface com Observable do hood, exceto que nós importamos Debug.Observe ao invés de Debug.Hood.Observe. Não vou mostrar código de exemplo porque ele é literalmente só copiar/colar o código antigo e mudar o import. Problemas com cópia e cola como essa (que também apareceram no dia 15 sobre IOSpec) de fato me fazem desejar que o Haskell tivesse módulos parametrizados como o ML. (E o problema com a instância orfã de Observable também, onde módulos estilo ML são a forma natural de plugar formas diferentes de observar o mesmo tipo de dados como for desejado).

GHood é um backend gráfico escrito em Java para o hood. Quando você instala o GHood, ele vem com um arquivo Java JAR. O Java lê um arquivo de logs ObserveEvents.log que é gerado a partir do código instrumentalizado pelo hood. Você pode animar, dar pulos para trás e para frente e os traces são mostrados em uma estrutura de árvore, incluindo mostrar a avaliação dos thunks. É uma prova-de-conceito interessante de 2001, mas é um pouco primitiva. Eu decidi não tentar incluir um screenshot de uma execução de exemplo, porque a saída do app Java Swing vai bem para a direita da janela e não é muito customizável.

Hoed

Hoed tem uma API baseada no hood mas é um projeto muito mais moderno, sofisticado e ativo. Ele permite “debugging algorítmo” fornecendo uma aplicação web interativa, onde usa uma estrutura de árvore para perguntar repetidamente ao usuário se os resultados estão corretos, para isolar e identificar a fonte do erro baseado no seu feedback.

Além disso, ele vem incluso com suporte para QuickCheck e debugging baseado propriedades. Confira a documentação e screenshots.

Hoed começa um servidor web local que você pode acessar com um browser na porta 10000. O repositório no GitHub contém um monte de exemplos.

Infelizmente, meu tempo acabou antes de que eu conseguisse o fazer funcionar da forma que eu queria para mostrar como um sistema de debugging interativo aqui, então, o que eu posso dizer é que o Hoed é muito interessante e que planejo ir mais fundo nisso quando tiver o tempo. Vou atualizar esse post mais tarde.

Conclusão

Eu não usei muitas ferramentas de debugging nos útilmos anos, mas eu gostaria de mudar isso na medida em que encontrar formas de usar novas ferramentas que sejam fáceis de usar. Sistemas baseados no hood parecem úteis como formas de coletar informação durante a execução sem ter que reestruturar o código radicalmente.

Todo o código

Todo o código para a série estará nesse repositório do GitHub.


Nota do tradutor

Se você quer ajudar com esse tipo de coisa, agora é a hora. Entre no Slack ou no IRC da HaskellBR e contribua. Esse blog e outros projetos associados estão na organização haskellbr no GitHub e em haskellbr.com/git.

Há um milestone no GitHub com tarefas esperando por você..

Share Comente no Twitter