Perché le liste contro sono associate alla programmazione funzionale?


22

Ho notato che la maggior parte dei linguaggi funzionali utilizza un elenco collegato singolarmente (un elenco "contro") come tipi di elenchi più fondamentali. Gli esempi includono Common Lisp, Haskell e F #. Ciò è diverso dalle lingue tradizionali, in cui i tipi di elenchi nativi sono array.

Perché?

Per Common Lisp (essendo digitato in modo dinamico) ho l'idea che i contro siano abbastanza generici da essere anche la base di liste, alberi, ecc. Questo potrebbe essere un piccolo motivo.

Per le lingue tipicamente statiche, tuttavia, non riesco a trovare un buon ragionamento, posso persino trovare contro-argomenti:

  • Lo stile funzionale incoraggia l'immutabilità, quindi la facilità di inserimento dell'elenco collegato è meno vantaggiosa,
  • Lo stile funzionale incoraggia l'immutabilità, quindi anche la condivisione dei dati; un array è più facile da condividere "parzialmente" di un elenco collegato,
  • Puoi anche fare corrispondenze di pattern su un array normale, e ancora meglio (potresti facilmente piegare da destra a sinistra per esempio),
  • Inoltre, ottieni l'accesso casuale gratuito,
  • E (un vantaggio pratico) se la lingua è digitata staticamente, puoi utilizzare un normale layout di memoria e ottenere un aumento di velocità dalla cache.

Quindi perché preferire gli elenchi collegati?


4
Provenendo dai commenti sulla risposta di @ sepp2k, penso che an array is easier to share "partially" than a linked listoccorra chiarire cosa intendi. A causa della loro natura ricorsiva, è vero il contrario, per quanto io lo capisca: è possibile condividere parzialmente un elenco collegato più facilmente passando lungo qualsiasi nodo in esso, mentre un array dovrebbe impiegare del tempo per fare una nuova copia. O in termini di condivisione dei dati, due elenchi collegati possono puntare allo stesso suffisso, il che è semplicemente impossibile con gli array.
Izkata,

Se un array si definisce come offset, lunghezza, buffer triplo, è possibile condividere un array creandone uno nuovo con offset + 1, lunghezza-1, buffer. Oppure utilizza un tipo speciale di array come subarray.
Dobes Vandermeer,

@Izkata Quando parliamo di array, raramente intendiamo solo un buffer come un puntatore all'inizio della memoria contigua in C. Di solito intendiamo una sorta di struttura che memorizza la lunghezza e un puntatore all'inizio di un buffer avvolto. In tale sistema, un'operazione di slicing può restituire un subarray, il cui puntatore del buffer punta a metà nel buffer (al primo elemento dell'array secondario) e il cui conteggio è tale che start + count fornisce l'ultimo elemento. Tali operazioni di taglio sono O (1) nel tempo e nello spazio
Alexander - Reinstate Monica il

Risposte:


22

Il fattore più importante è che puoi anteporre a un elenco immutabile collegato singolarmente in O (1) time, che ti consente di costruire ricorsivamente elenchi di n-element in O (n) time in questo modo:

// Build a list containing the numbers 1 to n:
foo(0) = []
foo(n) = cons(n, foo(n-1))

Se lo facessi usando array immutabili, il runtime sarebbe quadratico perché ogni consoperazione avrebbe bisogno di copiare l'intero array, portando a un tempo di esecuzione quadratico.

Lo stile funzionale incoraggia l'immutabilità, quindi anche la condivisione dei dati; un array è più facile da condividere "parzialmente" di un elenco collegato

Suppongo che condividendo "parzialmente" intendi che puoi prendere un subarray da un array in O (1) tempo, mentre con gli elenchi collegati puoi prendere la coda solo in O (1) tempo e tutto il resto ha bisogno di O (n). Questo è vero.

Comunque prendere la coda è abbastanza in molti casi. E devi tenere conto del fatto che essere in grado di creare a basso costo subarrays non ti aiuta se non hai modo di creare array a basso costo. E (senza intelligenti ottimizzazioni del compilatore) non c'è modo di costruire un array in modo economico passo dopo passo.


Questo non è affatto vero. È possibile aggiungere alle matrici in O ammortizzato (1).
DeadMG

10
@DeadMG Sì, ma non per array immutabili .
sepp2k,

"condivisione parziale" - Penso sia che due elenchi di contro possano puntare allo stesso elenco di suffissi (non so perché lo vorresti) e che puoi passare un punto medio anziché l'inizio dell'elenco a un'altra funzione senza dover copiare (l'ho fatto molte volte)
Izkata,

@Izkata L'OP parlava però di condividere parzialmente array, non elenchi. Inoltre non ho mai sentito quello che stai descrivendo come condivisione parziale . È solo condivisione.
sepp2k,

1
@Izkata L'OP utilizza il termine "array" esattamente tre volte. Una volta per dire che le lingue FP usano elenchi collegati in cui altre lingue usano array. Una volta per dire che gli array sono migliori nella condivisione parziale rispetto agli elenchi collegati e una volta per dire che gli array possono essere abbinati allo stesso modo (come gli elenchi collegati). In tutti i casi ha matrici e liste collegate contrastanti (per sottolineare che le matrici sarebbero più utili come struttura di dati primaria rispetto alle liste collegate, portando alla sua domanda perché le liste collegate sono preferite in FP), quindi non vedo come potrebbe usare i termini in modo intercambiabile.
sepp2k,

4

Penso che si tratti di elenchi che possono essere facilmente implementati nel codice funzionale.

Schema:

(define (cons x y)(lambda (m) (m x y)))

Haskell:

data  [a]  =  [] | a : [a]

Le matrici sono più difficili e non altrettanto belle da implementare. Se vuoi che siano estremamente veloci, dovranno essere scritti a basso livello.

Inoltre, la ricorsione funziona molto meglio sugli elenchi rispetto agli array. Considera il numero di volte in cui hai consumato / generato ricorsivamente un elenco rispetto a un array indicizzato.


Non direi che sia accurato chiamare la versione del tuo schema un'implementazione di elenchi collegati. Non sarai in grado di usarlo per memorizzare altro che funzioni. Inoltre, le matrici sono più difficili (in realtà impossibili) da implementare in qualsiasi linguaggio che non ha il supporto incorporato per loro (o blocchi di memoria), mentre gli elenchi collegati richiedono solo qualcosa come strutture, classi, record o tipi di dati algebrici da implementare. Questo non è specifico per i linguaggi di programmazione funzionale.
sepp2k,

@ sepp2k Cosa intendi con "archivia tutto tranne le funzioni" ?
Pubblico il

1
Ciò che intendevo dire era che gli elenchi definiti in questo modo non possono memorizzare nulla che non sia una funzione. Questo non è in realtà vero però. Non so, perché l'ho pensato. Mi dispiace per quello.
sepp2k,

2

Un elenco collegato singolarmente è la struttura di dati persistente più semplice .

Strutture di dati persistenti sono essenziali per una programmazione efficiente e puramente funzionale.


1
questo sembra semplicemente ripetere il punto sollevato e spiegato nella risposta migliore che è stata pubblicata più di 4 anni fa
moscerino

3
@gnat: la risposta principale non menziona le strutture di dati persistenti o che gli elenchi collegati singolarmente sono la struttura di dati persistenti più semplice o che sono essenziali per una programmazione puramente funzionale performante. Non riesco a trovare alcuna sovrapposizione con la risposta migliore.
Michael Shaw,

2

È possibile utilizzare facilmente i nodi Contro solo se si dispone di un linguaggio garbage collection.

Contro I nodi corrispondono molto allo stile di programmazione funzionale di chiamate ricorsive e valori immutabili. Quindi si adatta bene al modello del programmatore mentale.

E non dimenticare le ragioni storiche. Perché sono ancora chiamati contro nodi e peggio ancora usano auto e CDR come accessori? Le persone lo imparano da libri di testo e corsi e poi lo usano.

Hai ragione, nel mondo reale gli array sono molto più facili da usare, consumano solo metà dello spazio di memoria e sono molto performanter a causa di errori a livello di cache. Non c'è motivo di usarli con le lingue imperative.


1

Gli elenchi collegati sono importanti per il seguente motivo:

Una volta che prendi un numero come 3 e lo converti in una sequenza successiva come succ(succ(succ(zero))), quindi usi la sostituzione con esso {succ=List node with some memory space}e {zero = end of list}, finirai con un elenco collegato (di lunghezza 3).

La parte importante effettiva è numeri, sostituzione, spazio di memoria e zero.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.