Elenca Rock
La struttura di gran lunga più amichevole per i dati sequenziali in Haskell è l'Elenco
data [a] = a:[a] | []
Gli elenchi ti danno ϴ (1) contro e corrispondenza dei pattern. La libreria standard, e per questo il preludio, è pieno di funzioni utile elenco che dovrebbe lettiera il codice ( foldr
, map
, filter
). Le liste sono persistenti , anche puramente funzionali, il che è molto bello. Le liste di Haskell non sono in realtà "liste" perché sono coinduttive (altre lingue chiamano questi flussi) quindi cose del genere
ones :: [Integer]
ones = 1:ones
twos = map (+1) ones
tenTwos = take 10 twos
funziona meravigliosamente. Strutture di dati infinite oscillano.
Gli elenchi di Haskell forniscono un'interfaccia molto simile agli iteratori delle lingue imperative (a causa della pigrizia). Quindi, ha senso che siano ampiamente utilizzati.
D'altro canto
Il primo problema con le liste è che indicizzarle (!!)
richiede ϴ (k) tempo, il che è fastidioso. Inoltre, le appendici possono essere lente ++
, ma il modello di valutazione pigro di Haskell significa che possono essere trattate come completamente ammortizzate, se accadono affatto.
Il secondo problema con gli elenchi è che hanno una scarsa localizzazione dei dati. I processori reali presentano costanti elevate quando gli oggetti in memoria non sono disposti uno accanto all'altro. Quindi, in C ++ std::vector
ha "snoc" più veloce (mettendo gli oggetti alla fine) rispetto a qualsiasi struttura di dati di elenchi collegati pura che io conosca, sebbene questa non sia una struttura di dati persistenti così meno amichevole rispetto agli elenchi di Haskell.
Il terzo problema con le liste è che hanno scarsa efficienza di spazio. Mazzi di puntatori extra aumentano la tua memoria (di un fattore costante).
Le sequenze sono funzionali
Data.Sequence
è internamente basato su alberi delle dita (lo so, non lo vuoi sapere), il che significa che hanno delle belle proprietà
- Puramente funzionale.
Data.Sequence
è una struttura di dati completamente persistente.
- Accedi rapidamente all'inizio e alla fine dell'albero. ϴ (1) (ammortizzato) per ottenere il primo o l'ultimo elemento o per aggiungere alberi. Al momento gli elenchi delle cose sono più veloci,
Data.Sequence
al massimo è costantemente più lento.
- ϴ (registro n) accesso al centro della sequenza. Ciò include l'inserimento di valori per creare nuove sequenze
- API di alta qualità
D'altra parte, Data.Sequence
non fa molto per il problema della localizzazione dei dati e funziona solo per raccolte finite (è meno pigro degli elenchi)
Le matrici non sono per i deboli di cuore
Le matrici sono una delle strutture di dati più importanti in CS, ma non si adattano molto bene al pigro mondo funzionale puro. Le matrici forniscono ϴ (1) accesso al centro della raccolta e una localizzazione dei dati eccezionalmente buona / fattori costanti. Ma dal momento che non si adattano molto bene a Haskell, sono un dolore da usare. Ci sono in realtà una moltitudine di diversi tipi di array nella libreria standard corrente. Questi includono array completamente persistenti, array mutabili per la monade IO, array mutabili per la monade ST e versioni non boxate di quanto sopra. Per ulteriori informazioni, consulta il wiki di haskell
Il vettore è un array "migliore"
Il Data.Vector
pacchetto fornisce tutta la bontà dell'array, in un'API di livello superiore e più pulita. A meno che tu non sappia davvero cosa stai facendo, dovresti usarli se hai bisogno di prestazioni simili ad array. Ovviamente, si applicano ancora alcuni avvertimenti: array mutabili come strutture di dati non giocano proprio in linguaggi pigri puri. Tuttavia, a volte vuoi quella O (1) performance e te la Data.Vector
offre in un pacchetto utilizzabile.
Hai altre opzioni
Se desideri solo elenchi con la possibilità di inserirli in modo efficiente alla fine, puoi utilizzare un elenco di differenze . Il miglior esempio di elenchi che rovinano le prestazioni tende a venire da [Char]
cui il preludio è aliasato String
. Char
gli elenchi sono comodi, ma tendono a funzionare nell'ordine 20 volte più lento delle stringhe C, quindi sentiti libero di usare Data.Text
o molto veloce Data.ByteString
. Sono sicuro che ci sono altre librerie orientate alla sequenza a cui non sto pensando in questo momento.
Conclusione
Il 90% delle volte in cui ho bisogno di una raccolta sequenziale negli elenchi di Haskell è la giusta struttura di dati. Gli elenchi sono come gli iteratori, le funzioni che consumano elenchi possono essere facilmente utilizzate con una qualsiasi di queste altre strutture di dati utilizzando le toList
funzioni fornite. In un mondo migliore il preludio sarebbe completamente parametrico sul tipo di contenitore che utilizza, ma attualmente []
sporca la libreria standard. Quindi, usare le liste (quasi) ogni dove va decisamente bene.
Puoi ottenere versioni completamente parametriche della maggior parte delle funzioni dell'elenco (e non puoi usarle)
Prelude.map ---> Prelude.fmap (works for every Functor)
Prelude.foldr/foldl/etc ---> Data.Foldable.foldr/foldl/etc
Prelude.sequence ---> Data.Traversable.sequence
etc
In effetti, Data.Traversable
definisce un'API che è più o meno universale in qualsiasi cosa "elenca".
Tuttavia, sebbene tu possa essere bravo e scrivere solo codice completamente parametrico, la maggior parte di noi non lo è e usa la lista ovunque. Se stai imparando, ti consiglio vivamente di farlo anche tu.
EDIT: Sulla base di commenti mi rendo conto che non ho mai spiegato quando utilizzare Data.Vector
vs Data.Sequence
. Le matrici e i vettori forniscono operazioni di indicizzazione e suddivisione estremamente veloci, ma sono strutture di dati fondamentalmente transitorie (imperative). Strutture di dati funzionali puri gradiscono Data.Sequence
e []
consentono di produrre in modo efficiente nuovi valori da vecchi valori come se avessi modificato i vecchi valori.
newList oldList = 7 : drop 5 oldList
non modifica il vecchio elenco e non deve copiarlo. Quindi, anche se oldList
è incredibilmente lunga, questa "modifica" sarà molto veloce. allo stesso modo
newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence
produrrà una nuova sequenza con un newValue
for al posto del suo 3000 elemento. Ancora una volta, non distrugge la vecchia sequenza, ma ne crea solo una nuova. Ma lo fa in modo molto efficiente, prendendo O (log (min (k, kn)) dove n è la lunghezza della sequenza e k è l'indice che si modifica.
Non puoi farlo facilmente con Vectors
e Arrays
. Possono essere modificati, ma questa è una vera e propria modifica imperativa, e quindi non può essere eseguita nel normale codice Haskell. Ciò significa che le operazioni nel Vector
pacchetto che apportano modifiche come snoc
e cons
devono copiare l'intero vettore, quindi richiedono O(n)
tempo. L'unica eccezione a questo è che puoi usare la versione mutabile ( Vector.Mutable
) all'interno della ST
monade (o IO
) e fare tutte le tue modifiche proprio come faresti in un linguaggio imperativo. Quando hai finito, "congeli" il tuo vettore per trasformarlo nella struttura immutabile che vuoi usare con puro codice.
La mia sensazione è che dovresti usare di default Data.Sequence
se un elenco non è appropriato. Utilizzare Data.Vector
solo se il proprio modello di utilizzo non comporta l'esecuzione di numerose modifiche o se sono necessarie prestazioni estremamente elevate all'interno delle monadi ST / IO.
Se tutti questi discorsi sulla ST
monade ti lasciano confuso: una ragione in più per restare fedeli al puro, veloce e bello Data.Sequence
.