Bene, se guardi un po 'più in profondità, entrambi in realtà includono anche array nella lingua di base:
- Il 5 ° Rapporto sullo schema rivisto (R5RS) include il tipo di vettore , che sono raccolte indicizzate per numeri interi di dimensioni fisse con tempi migliori di quelli lineari per l'accesso casuale.
- Anche il report Haskell 98 ha un tipo di array .
Le istruzioni di programmazione funzionale, tuttavia, hanno da sempre enfatizzato gli elenchi a collegamento singolo su array o elenchi a doppio collegamento. Molto probabilmente enfatizzato, in effetti. Ci sono diverse ragioni per questo, comunque.
Il primo è che gli elenchi a collegamento singolo sono uno dei tipi di dati ricorsivi più semplici ma utili. Un equivalente definito dall'utente del tipo di elenco di Haskell può essere definito in questo modo:
data List a -- A list with element type `a`...
= Empty -- is either the empty list...
| Cell a (List a) -- or a pair with an `a` and the rest of the list.
Il fatto che gli elenchi siano un tipo di dati ricorsivo significa che le funzioni che lavorano sugli elenchi generalmente utilizzano la ricorsione strutturale . In termini di Haskell: il modello corrisponde ai costruttori di elenchi e si ricorre su una sottoparte dell'elenco. In queste due definizioni di funzioni di base, utilizzo la variabile as
per fare riferimento alla coda dell'elenco. Quindi nota che le chiamate ricorsive "discendono" in fondo alla lista:
map :: (a -> b) -> List a -> List b
map f Empty = Empty
map f (Cell a as) = Cell (f a) (map f as)
filter :: (a -> Bool) -> List a -> List a
filter p Empty = Empty
filter p (Cell a as)
| p a = Cell a (filter p as)
| otherwise = filter p as
Questa tecnica garantisce che la tua funzione terminerà per tutti gli elenchi finiti, ed è anche una buona tecnica di risoluzione dei problemi: tende naturalmente a dividere i problemi in sottoparti più semplici e sostenibili.
Quindi gli elenchi a collegamento singolo sono probabilmente il miglior tipo di dati per presentare agli studenti queste tecniche, che sono molto importanti nella programmazione funzionale.
Il secondo motivo è meno un motivo "perché elenchi a collegamento singolo", ma più un motivo "perché non elenchi o collegamenti a doppio collegamento": questi ultimi tipi di dati richiedono spesso la mutazione (variabili modificabili), che la programmazione funzionale molto spesso si allontana da. Così come succede:
- In una lingua entusiasta come Scheme non è possibile creare un elenco con doppio collegamento senza usare la mutazione.
- In un linguaggio pigro come Haskell puoi creare un elenco con doppio link senza usare la mutazione. Ma ogni volta che crei un nuovo elenco basato su quello, sei costretto a copiare la maggior parte se non tutta la struttura dell'originale. Considerando che con gli elenchi a collegamento singolo è possibile scrivere funzioni che utilizzano la "condivisione della struttura": i nuovi elenchi possono riutilizzare le celle dei vecchi elenchi quando appropriato.
- Tradizionalmente, se si utilizzavano array in modo immutabile, ciò significava che ogni volta che si desiderava modificare l'array, si doveva copiare tutto. (Le librerie recenti di Haskell come
vector
, tuttavia, hanno trovato tecniche che migliorano notevolmente su questo problema).
Il terzo e ultimo motivo si applica principalmente ai linguaggi pigri come Haskell: gli elenchi pigri a collegamento singolo, in pratica, sono spesso più simili agli iteratori che agli elenchi in memoria propri. Se il tuo codice sta consumando gli elementi di un elenco in sequenza e li butta via man mano che procedi, il codice oggetto materializzerà solo le celle dell'elenco e il suo contenuto man mano che avanzi nell'elenco.
Ciò significa che l'intero elenco non deve necessariamente esistere in memoria contemporaneamente, solo la cella corrente. Le celle precedenti a quella corrente possono essere raccolte in modo inutile (cosa impossibile con un elenco a doppio collegamento); le celle successive a quella attuale non devono essere calcolate finché non ci si arriva.
Va anche oltre. Esiste una tecnica utilizzata in diverse librerie Haskell popolari, chiamata fusion , in cui il compilatore analizza il codice di elaborazione degli elenchi e individua gli elenchi intermedi che vengono generati e consumati in sequenza e quindi "eliminati". Con questa conoscenza, il compilatore può eliminare completamente l'allocazione di memoria delle celle di tali elenchi. Ciò significa che un elenco a collegamento singolo in un programma sorgente Haskell, dopo la compilazione, potrebbe effettivamente essere trasformato in un ciclo anziché in una struttura di dati.
La fusione è anche la tecnica che la vector
libreria di cui sopra utilizza per generare codice efficiente per array immutabili. Lo stesso vale per le librerie estremamente popolari bytestring
(array di byte) e text
(stringhe Unicode), che sono state costruite in sostituzione del String
tipo nativo non molto grande di Haskell (che è lo stesso [Char]
dell'elenco di caratteri a collegamento singolo). Quindi nella moderna Haskell c'è una tendenza in cui i tipi di array immutabili con supporto alla fusione stanno diventando molto comuni.
La fusione delle liste è facilitata dal fatto che in una lista a link singolo è possibile andare avanti ma mai indietro . Questo fa apparire un tema molto importante nella programmazione funzionale: usare la "forma" di un tipo di dati per ricavare la "forma" di un calcolo. Se si desidera elaborare gli elementi in sequenza, un elenco a collegamento singolo è un tipo di dati che, quando lo si consuma con ricorsione strutturale, offre tale modello di accesso in modo molto naturale. Se si desidera utilizzare una strategia di "divisione e conquista" per attaccare un problema, le strutture dati dell'albero tendono a supportarlo molto bene.
Molte persone abbandonano presto il vagone di programmazione funzionale, quindi ottengono esposizione alle liste a link singolo ma non alle idee sottostanti più avanzate.