Listas de rock
De longe, a estrutura de dados mais amigável para dados seqüenciais em Haskell é a Lista
data [a] = a:[a] | []
As listas fornecem cons (1) contras e correspondência de padrões. A biblioteca padrão, e para que o assunto o prelúdio, é cheio de funções de lista úteis que deve ninhada seu código ( foldr
, map
, filter
). As listas são persistentes , também conhecidas como puramente funcionais, o que é muito bom. As listas de Haskell não são realmente "listas" porque são coindutivas (outros idiomas chamam esses fluxos), então coisas como
ones :: [Integer]
ones = 1:ones
twos = map (+1) ones
tenTwos = take 10 twos
trabalhar maravilhosamente. Estruturas infinitas de dados.
As listas em Haskell fornecem uma interface semelhante aos iteradores em linguagens imperativas (devido à preguiça). Portanto, faz sentido que eles sejam amplamente utilizados.
Por outro lado
O primeiro problema com as listas é que indexá-las (!!)
leva ϴ (k), o que é irritante. Além disso, os anexos podem ser lentos++
, mas o modelo de avaliação preguiçoso de Haskell significa que eles podem ser tratados como totalmente amortizados, se acontecerem.
O segundo problema com as listas é que elas têm pouca localidade de dados. Processadores reais incorrem em altas constantes quando objetos na memória não são dispostos um ao lado do outro. Portanto, em C ++ std::vector
, o "snoc" é mais rápido (colocando objetos no final) do que qualquer estrutura de dados de lista vinculada pura que eu conheço, embora essa não seja uma estrutura de dados persistente tão menos amigável que as listas de Haskell.
O terceiro problema com as listas é que elas têm pouca eficiência de espaço. Muitos ponteiros extras aumentam seu armazenamento (por um fator constante).
Sequências são funcionais
Data.Sequence
é baseado internamente em árvores de dedos (eu sei, você não quer saber disso), o que significa que elas têm algumas propriedades legais
- Puramente funcional.
Data.Sequence
é uma estrutura de dados totalmente persistente.
- Acesso rápido ao início e fim da árvore. ϴ (1) (amortizado) para obter o primeiro ou o último elemento ou anexar árvores. Nas listas de coisas mais rápidas,
Data.Sequence
é no máximo uma constante mais lenta.
- Log (log n) acesso ao meio da sequência. Isso inclui inserir valores para criar novas seqüências
- API de alta qualidade
Por outro lado, Data.Sequence
não faz muito pelo problema da localidade dos dados e funciona apenas para coleções finitas (é menos preguiçoso que as listas)
Matrizes não são para os fracos de coração
As matrizes são uma das estruturas de dados mais importantes no CS, mas não se encaixam muito bem no mundo funcional puro e preguiçoso. As matrizes fornecem ϴ (1) acesso ao meio da coleta e excepcionalmente boa localização de dados / fatores constantes. Mas, como eles não se encaixam muito bem em Haskell, eles são uma dor de usar. Na verdade, existem vários tipos diferentes de matrizes na biblioteca padrão atual. Isso inclui matrizes totalmente persistentes, matrizes mutáveis para a mônada de E / S, matrizes mutáveis para a mônada ST e versões sem caixa das opções acima. Para saber mais, confira o wiki do haskell
Vetor é uma matriz "melhor"
O Data.Vector
pacote fornece toda a qualidade da matriz, em uma API mais limpa e de nível superior. A menos que você realmente saiba o que está fazendo, use-os se precisar de uma matriz como desempenho. Obviamente, algumas advertências ainda se aplicam - array mutável, como estruturas de dados, simplesmente não funcionam bem em linguagens lentas e puras. Ainda assim, às vezes você deseja o desempenho O (1) e o Data.Vector
entrega em um pacote utilizável.
Você tem outras opções
Se você deseja apenas listas com a capacidade de inserir com eficiência no final, use uma lista de diferenças . O melhor exemplo de lista que estraga o desempenho tende a vir de [Char]
onde o prelúdio aliasou como String
. Char
As listas são convenientes, mas tendem a ser executadas na ordem 20 vezes mais lenta que as seqüências C, portanto, fique à vontade para usar Data.Text
ou muito rapidamente Data.ByteString
. Tenho certeza de que existem outras bibliotecas orientadas a sequência nas quais não estou pensando agora.
Conclusão
Mais de 90% do tempo em que preciso de uma coleção seqüencial nas listas Haskell são a estrutura de dados correta. As listas são como iteradores, funções que consomem listas podem ser facilmente usadas com qualquer uma dessas outras estruturas de dados, usando as toList
funções fornecidas. Em um mundo melhor, o prelúdio seria totalmente paramétrico quanto ao tipo de contêiner que ele usa, mas atualmente []
desarruma a biblioteca padrão. Então, usar listas (quase) em todos os lugares é definitivamente bom.
Você pode obter versões totalmente paramétricas da maioria das funções da lista (e é nobre usá-las)
Prelude.map ---> Prelude.fmap (works for every Functor)
Prelude.foldr/foldl/etc ---> Data.Foldable.foldr/foldl/etc
Prelude.sequence ---> Data.Traversable.sequence
etc
De fato, Data.Traversable
define uma API que é mais ou menos universal em qualquer "lista como".
Ainda assim, embora você possa ser bom e escrever apenas códigos totalmente paramétricos, a maioria de nós não é e usa a lista em todo o lugar. Se você está aprendendo, sugiro fortemente que você também.
EDIT: Com base nos comentários eu percebo que eu nunca explicou quando usar Data.Vector
vs Data.Sequence
. Matrizes e vetores fornecem operações de indexação e divisão extremamente rápidas, mas são estruturas de dados fundamentalmente transitórias (imperativas). Estruturas de dados funcionais puras gostam Data.Sequence
e []
permitem produzir eficientemente novos valores a partir de valores antigos, como se você tivesse modificado os valores antigos.
newList oldList = 7 : drop 5 oldList
não modifica a lista antiga e não precisa copiá-la. Portanto, mesmo que oldList
seja incrivelmente longa, essa "modificação" será muito rápida. similarmente
newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence
produzirá uma nova sequência com a newValue
no lugar de seu elemento 3000. Novamente, não destrói a sequência antiga, apenas cria uma nova. Mas isso é feito com muita eficiência, usando O (log (min (k, kn)), em que n é o comprimento da sequência ek é o índice que você modifica.
Você não pode fazer isso facilmente com Vectors
e Arrays
. Eles podem ser modificados, mas essa é uma modificação imperativa real e, portanto, não pode ser feita no código Haskell regular. Isso significa operações no Vector
pacote que fazem modificações snoc
e cons
precisam copiar o vetor inteiro, para levar O(n)
tempo. A única exceção é que você pode usar a versão mutável ( Vector.Mutable
) dentro da ST
mônada (ou IO
) e fazer todas as suas modificações como faria em uma linguagem imperativa. Quando você termina, "congela" seu vetor para se transformar na estrutura imutável que deseja usar com código puro.
Meu sentimento é que você deve usar o padrão Data.Sequence
se uma lista não for apropriada. Use Data.Vector
apenas se o seu padrão de uso não envolver muitas modificações ou se você precisar de desempenho extremamente alto nas mônadas do ST / IO.
Se toda essa conversa sobre a ST
mônada está deixando você confuso: mais um motivo para se ater ao puro rápido e bonito Data.Sequence
.