24 dias de Hackage, 2015 - dia 18 - vector, vector-algorithms: Libere seu alter-programador C
por Franklin Chen
traduzido por Pedro Yamada
Esse é um artigo escrito por Franklin Chen e traduzido para o português. Ler original.
Encontro HaskellBR São Paulo
Índice de toda a série
O índice de toda a série está no topo do artigo para o dia 1.
Dia 18
É comum em programação, que a estrutura de dados a ser usada é um array estilo C, seja ele tratado como mutável ou imutável.
Idealmente, eu não teria que escrever esse artigo, mas eu tenho a impressão de
que muitos recem chegados ao Haskell não sabem sobre o
vector
, a biblioteca para
arrays estilo C, e usam listas onde arrays se encaixariam melhor no
problema. Arrays são eficientes para indexar e também são eficientes quanto
ao cache da CPU e a memória, sendo alocados em um bloco de memória, ao invés de
espalhados por meio de um monte de ponteiros.
A terminologia usada para arrays no ecossistema do Haskell é confusa porque
Haskell, nos anos 90, originalmente veio com uma estrutura de dados chamada
Array
, e há até um pacote
array
,
mas na prática eu nunco o uso porque ele é mais genérico e estranho que a
estrutura de dados simples exposta mais tarde chamada “vetores” [N.T. “vectors”]
(pela falta de um nome melhor).
Há bons tutoriais sobre o vector
que eu recomendo:
Achei que seria útil dar um exemplo completo de usar vetores imutáveis e mutáveis. Em particular, vamos usar vetores unboxed que são especializados e, portanto, não contém nenhuma indireção e são basicamente equivalentes aos arrays estilo C. Vamos fazer um pouco de programação de baixo nível estilo C.
O problema a ser resolvido: encontrar a mediana de um array de inteiros de 8-bits
Aqui está um problema encontrado no Programming Praxis:
A mediana de um array é o valor no meio se o array estivesse ordenado: se o array tiver um número impar de items
n
, a mediana é o(n+1)/2
nth maior item no array (que também é o(n+1)/2
nth menor item no array), e se o array tiver um número par de itemsn
, a mediana é a média aritmética don/2
th menor item e don/2
th maior item no array. Por exemplo, a mediana do array[2,4,5,7,3,6,1]
é4
e a mediana do array[5,2,1,6,3,4]
é3.5
.Sua tarefa é escrever um programa que recebe um array de inteiros de 8-bits (possivelmente mas não necessariamente ordenados) e encontra o valor da mediana do array; você deve encontrar um algoritmo que tome tempo linear e espaço constante.
Instalação do vector
O Stackage LTS ainda está no vector-0.10
, mas eu queria usar a
versão 0.11 porque a API
mudou um pouco, então eu adicionei uma restrição de versão apropriada ao
arquivo Cabal.
A importância de ir unboxed
Já que temos que escrever código específico a um array de inteiros de 8-bits, vamos usar vetores unboxed ao invés de boxed (genéricos): vetores boxed contém elementos que são ponteiros para valores, enquanto os unboxed tem elementos que são os próprios valores. Isso é importante para a eficiência, se você sabe de cara qual é o valor dos seus elementos.
Alguns testes
Vamos escrever alguns testes antes de mais nada, assumindo que vamos escrever
um módulo VectorExample
.
Primeiro, alguns imports. Nós vamos usar o operador de indexing em tempo
constante para vetores !
.
Vamos usar HSpec e QuickCheck, e escrever nossos próprios geradores de vetores aleatórios:
Vamos implementar tanto uma versão simples de referência e a inteligente desejada, então vamos estar ambas:
Testamos os exemplos fornecidos, assim como propriedades genéricas. Para
ordenamento, nós convenientemente usamos a ordenação radix do pacote
vector-algorithms
:
Os testes de propriedades especificam o que a mediana deve ser, para todos os casos possíveis.
Uma nota sobre mutação
A ordenação radix opera sobre vetores mutáveis, mas estamos usando um vetor
função modify
que tem um tipo pomposo:
Ela recebe um callback que recebe um vetor mutável e transforma um vetor
imutável em outro vetor imutável (possivelmente o mesmo vetor mutado
“secretamente” por baixo dos panos). Usa tipos de alta ordem e o
monad transformer de estado ST
,
mas tudo o que você tem de saber é que
Radix.sort
tem um tipo compatível de callback e pode portanto ser composto com modify
:
Geradores customizados do QuickCheck
Aqui estão alguns geradores rápidos de vetores do tipo que queremos para os testes. Se estivéssemos fazendo isso de verdade, provavelmente gostariamos de escrever shrinkers customizados, ao invés de percorrer listas.
Uma versão ineficiente que viola o requisito de espaço
Para referência (nós podíamos quase ler a partir dos testes do QuickCheck):
Uma nota sobre operações unsafe
Eu prometi que iríamos fazer programação estilo C. Poderíamos usar indexing
com um bounds-check (que atira uma exceção durante runtime quando acessamos
elementos que não estão em um vetor), mas vamos nos “divertir” um pouco, e usar
o unsafeIndex
. Note que isso é inseguro mesmo: vamos pegar o centésimo
elemento de um vetor com 1 Int
:
Porque usar operações unsafe? Para ilustração e também porque acontece de que podemos provar (fora do type system) que nós nunca vamos tentar acessar um elemento fora do vetor. Na vida real, eu hesitaria em usar operações unsafe a não ser que ter a performance máxima fosse crítico! Mas nós podemos nos candidatar ao mundo de desastres em potencial do C se quisermos.
Não quero promover usar operações unsafe!! Substitua todas as funções
de hoje chamadas unsafeXyz
por xyz
para ficar tranquilo.
Uma solução inteligente
A função inteligente aloca uma tabela de todas as contegens possíveis (porque sabemos que os valores são inteiros unsigned de 8-bits) e a preenche, então itera pela tabela e descobre onde a mediana deve estar, e quando bate no lugar certo, retorna o valor do meio (se tiver um número impar de valores) ou o meio de dois valores (se tiver um número par de valores).
Note o uso da extensão do GHC BangPatterns
, coberta em um
dia de extensões do GHC de 2014,
para nos assegurarmos da avaliação estrita em loops recursivos “pela cauda”
(tail-recursive loops).
Formas mutáveis e imutáveis de criar a tabela de contagens
O que é makeCountsMutably
?
Mutável
A versão da função que usa mutação usa a
função create
:
create
tem tipo:
E inteligentemente “congela” o vetor retornado no lugar, dentro do contexto do
monad ST
, em um vetor imutável, o que evita qualquer cópia, porque a mutação
acontece dentro do callback e não é visível fora dele.
Imutável
Mas espere, também há uma forma de criar a tabela usando somente Vector
s
imutáveis, sem nenhum vetor MVector
intermediário!
Isso é elegante de direto, mas parece criar um vetor intermediário usando
map
, que não é o que queremos já que todo o ponto é ter eficiência de espaço.
Fusion
Acontece que o vector
vem com sofisticado
suporte para fusion.
Um dia de Hackage posterior (agora disponível no
dia 19)
vou esplicar o que acontece nos bastidores. Efetivamente, o GHC reescreve as
chamadas para unsafeAccumulate
e map
para evitar o vetor intermediário!
Conclusão
Arrays de tamanho fixo são uma peça importante para a construção muitas
computações. Uso o vector
quando preciso fazer operações (sequenciais) sobre
arrays. Espero que esse pequeno exemplo mostre como escrever código imperativo
baseado em vetores em Haskell, escondendo a maior quantidade de estado
possível, por meio de APIs orientadas a tipos que operam entre os dois mundos
da mutabilidade e imutabilidade. É até possível escrever código que dependa no
fusion e não crie vetores mutáveis intermediários.
(Esses são arrays sequenciais. Para arrays paralelos, veja um
dia de Hackage de 2013,
que cobriu o repa
, uma biblioteca
para “arrays paralelos de alta performance, regulares, multi-dimensionais e
polimórficos”.)
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.