24 dias de Hackage, 2015 - dia 8 - multiset; gostaria que isso estivesse no pacote padrão
por Franklin Chen
traduzido por Hanneli T.
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.
Dia 8
Não me lembro quando foi, mas um dia fiquei cansado de reimplementar, em Haskell, a mesma lógica clichê para rastrear múltiplos objetos sob a mesma chave. Essa estrutura de dados é conhecida por multiset, ou bag (N.T. alguns livros de estrutura de dados em Português podem traduzir para balde), em contraste com um set regular, que rastreia apenas um único objeto por chave.
Fiquei surpreso com a ausência de um módulo para multiset nos containers
do pacote padrão, embora seja possível implementar um multiset com base em um set, então é razoável que essa estrutura seja “supérflua”. Entretanto, estava acostumado a ter a estrutura de multiset pronta, sem nenhum trabalho (mesmo que trivial), devido ao constante uso da estrutura no C++, onde voltava aos anos 1990 para checar a implementação original da Standard Template Library, e também a biblioteca de collections do Python possui o Counter
, classe que serve para propósito similar.
Hoje brevemente mostrarei um código que utiliza o multiset
e falarei sobre por que uma estrutura assim talvez devesse estar na biblioteca padrão.
O exemplo clássico de uso do multiset
Multisets geralmente são usados para contar coisas. Contadores de palavras são exemplos consagrados de uso, então vamos implementar um simples contador de palavras que quebra um texto em palavras (“words”) separadas por espaços, e, tratando elementos diferentes de letras como espaços, conta as palavras e finalmente apresenta uma saída que consiste numa linha para cada palavra e sua contagem, em ordem descendente de número mas em ordem ascendente de palavra.
Para um pouco mais de realismo e eficiência mesmo para um projeto demonstrativo, não utilizemos o input como String
, mas como sendo do tipo Text
, a partir do módulo Data.Text.Lazy
do pacote text
. Vamos construir também um relatório final como Text
, igualmente eficiente.
Uma amostra de teste com HSpec
Temos aqui um teste (por diversão talvez você queira escrever alguns geradores com QuickCheck e uma suite de testes para esse problema). Peço desculpas pela quase ilegível String de múltiplas linhas utilizada; gostaria que o Haskell possuísse uma estrutura para strings de múltiplas linhas, interpolação e todos esses outros manipuladores úteis que outras linguagens possuem.
(Tangenciando o dia 9)
Algumas pessoas comentaram que fiz um comentário errôneo anteriormente sobre o suporte a strings em Haskell. É fato que a sintaxe do core é limitada, mas é fácil contornar isso com o Template Haskell, portanto dê uma olhada no Dia 9 para ver como utilizá-lo. Recomendo fortemente o use de alguma das bibliotecas mencionadas.
Brevemente, a horrenda string poderia ser escrita como:
O código
A solução
O código praticamente se descreve como uma linguagem natural para o problema.
Vamos analisar passo a passo. Note que estamos usando meu operador de composição favorito no estilo left-to-right (esquerda para a direita), o >>>
, em oposição ao operador right-to-left .
(direita para a esquerda), pois o >>>
apresenta-se de forma muito mais natural para pipelines, e também porque minha descrição é feita do começo ao fim, combinando com a sintaxe.
(Atualização na sintaxe)
Aqui está o trechod e código com a sintaxe tradicional right-to-left (direita para esquerda). Acho que é importante estar apto a ler e escrever em ambos os estilos:
Coletando as palavras do texto
Primeiramente, substitua o que for diferente de letras por espaços em branco, atendendo ao propósito desse problema, e então utilize o words
do Data.Text.Lazy
(o qual importamos como LazyText
), para assim podermos separar as palavras (uma tokenização real teria regras mais sofisticadas). A propósito, isso é feito de forma bem eficiente oirque o GHC performa o fusion a fim de substituir e separar as palavras em uma única etapa, em vez de duas tal qual escrevemos no código. Fusion é uma otimização muito, muito legal para a qual confiamos nosso código Haskell se tornar super eficiente:
Colocando as palavras em um multiset
Na sequência, colocamos a lista de palavras em um multiset. MultiSet
convenientemente possui um construtor para isso, fromList :: Ord a => [a] -> MultiSet a
, e ele é executado em tempo O(n log n)
devido à comparação através de Ord
para construir uma árvore (exercite isso caso queira: use alguma das bibliotecas de tabela Hash no Hackage para criar um MultiHashSet
).
Contando as palavras
Coletamos a lista de pares de palavras/quantidade (em ordem ascendente de palavras) utilizando toOccurList :: MultiSet a -> [(a, Occur)]
, onde type Occur = Int
:
Ordenando a lista de palavras/quantidade
Nós ordenamos utilizando a “Transformada de Schwartzian”, eficientemente usando Data.List.sortOn
do tipo sortOn :: Ord b => (a -> b) -> [a] -> [a]
; recorde-se que queremos que a quantidade (o segundo elemento da tupla que obtemos do multiset) seja descendente.
A documentação do Data.Ord.Down
explica como esse “newtype hack” é usado para mudar a comparação padrão durante a ordenação.
Gerando um relatório para cada palavra/quantidade
onde nossa função resumo, para eficiência em evitar alocações desnecessárias, não constrói um Text
mas sim um builder, dado que não precisamos dessa informação para cada palavra como Text
, mas planejamos combinar os relatórios para todas as linahs como um único Text
no final.
(Se você está curioso(a) sobre como o builder funciona, dê uma olhada no código fonte. Utilizam-se tipos higher-rank, operações unsafe e anotações strict).
Gerando o relatório final
Apenas materialize o Text
a partir do builder:
O que deveria entrar na biblioteca padrão?
Mencionei que acho que isso deveria ir para a biblioteca padrão por conveniência. O argumento para não colocar algo assim como padrão é sempre o de todo mundo acha que seu código super útil deveria ir para lá também, mas não há meios para isso acontecer e escolhas devem ser feitas. Dado isso, não posso discutir baseado na minha experiência e percepção próprias de que a comunidade como um todo teria benefícios se meus feitos fossem padronizados. Gostaria de pensar que temos tecnologia para coletar dados reais que determinam as necessidades concretas das pessoas e mudam o modo descobrimos e utilizamos bibliotecas. Mesmo algo como as perguntas do Stack Overflow poderiam trazer estatísticas interessantes sobre o que as pessoas necessitam. Tal coleta de dados não substituiria um especialista, claro, mas certamente seria útil.
Há um outro argumento para não ter algo como o multiset incorporado na biblioteca padrão: é fácil implementá-lo uma vez que você tenha um set regular. Se você olhar o código fonte, notará que basicamente um multiset é apenas um mapa de uma chave para uma contagem ou quantidade.
Todo resto é apenas um wrapper bem direto.
Mas isso é como dizer que a linguagem não deveria ter nenhum facilitador sintático (“syntactic sugar”). Acho que o termo “library sugar” é tão valioso quanto “syntactic sugar”.
Conclusão
Multisets são úteis. Você pode implementá-los ou utilizar o pacote multiset
que alguma outra pessoa já escreveu para nós.
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.