Nella programmazione funzionale, avere la maggior parte delle strutture di dati immutabili richiede un maggiore utilizzo della memoria?


63

Nella programmazione funzionale poiché quasi tutte le strutture dati sono immutabili, quando lo stato deve cambiare viene creata una nuova struttura. Questo significa molto più utilizzo della memoria? Conosco bene il paradigma di programmazione orientata agli oggetti, ora sto cercando di conoscere il paradigma di programmazione funzionale. Il concetto di tutto ciò che è immutabile mi confonde. Sembrerebbe che un programma che utilizza strutture immutabili richiederebbe molta più memoria di un programma con strutture mutabili. Sto anche guardando questo nel modo giusto?


7
Si può dire che, ma le strutture di dati più immutabili riutilizzare i dati sottostanti per le modifiche. Eric Lippert ha una grande serie di blog sull'immutabilità in C #
Oded,

3
Darei un'occhiata a Strutture di dati puramente funzionali, è un ottimo libro che è stato scritto dallo stesso ragazzo che ha scritto la maggior parte della libreria di container di Haskell (anche se il libro è principalmente SML)
jozefg,

1
Questa risposta, correlata al tempo di esecuzione anziché al consumo di memoria, potrebbe anche essere interessante per te: stackoverflow.com/questions/1990464/…
9000

Risposte:


35

L'unica risposta corretta a questo è "a volte". Ci sono molti trucchi che i linguaggi funzionali possono usare per evitare di sprecare memoria. L'immutabilità semplifica la condivisione dei dati tra funzioni e persino tra strutture di dati, poiché il compilatore può garantire che i dati non vengano modificati. I linguaggi funzionali tendono a incoraggiare l'uso di strutture di dati che possono essere utilizzate in modo efficiente come strutture immutabili (ad esempio alberi invece di tabelle hash). Se aggiungi pigrizia al mix, come fanno molti linguaggi funzionali, questo aggiunge nuovi modi per risparmiare memoria (aggiunge anche nuovi modi di sprecare memoria, ma non ci entrerò).


24

Nella programmazione funzionale poiché quasi tutte le strutture dati sono immutabili, quando lo stato deve cambiare viene creata una nuova struttura. Questo significa molto più utilizzo della memoria?

Dipende dalla struttura dei dati, dalle modifiche esatte eseguite e, in alcuni casi, dall'ottimizzatore. Ad esempio, consideriamo di anteporre a un elenco:

list2 = prepend(42, list1) // list2 is now a list that contains 42 followed
                           // by the elements of list1. list1 is unchanged

Qui il requisito di memoria aggiuntiva è costante, così come il costo di runtime della chiamata prepend. Perché? Perché prependsemplicemente crea una nuova cellula che ha 42come testa e list1coda. Non è necessario copiare o iterare in altro modo list2per raggiungere questo obiettivo. Cioè, ad eccezione della memoria richiesta per l'archiviazione 42, list2riutilizza la stessa memoria utilizzata da list1. Poiché entrambi gli elenchi sono immutabili, questa condivisione è perfettamente sicura.

Allo stesso modo, quando si lavora con strutture ad albero bilanciate, la maggior parte delle operazioni richiede solo una quantità logaritmica di spazio aggiuntivo perché è possibile condividere qualsiasi cosa, tranne un percorso dell'albero.

Per gli array la situazione è leggermente diversa. Ecco perché, in molte lingue FP, le matrici non sono quelle comunemente usate. Tuttavia, se fai qualcosa di simile arr2 = map(f, arr1)e arr1non viene più utilizzato dopo questa riga, un ottimizzatore intelligente può effettivamente creare codice che muta arr1invece di creare un nuovo array (senza influire sul comportamento del programma). In tal caso, lo spettacolo sarà ovviamente come in un linguaggio imperativo.


1
Per interesse, quale implementazione di quali lingue riutilizzano lo spazio come hai descritto alla fine?

@delnan Alla mia università c'era un linguaggio di ricerca chiamato Qube, che lo faceva. Tuttavia, non so se esiste un linguaggio usato allo stato brado. Tuttavia, la fusione di Haskell può ottenere lo stesso effetto in molti casi.
sepp2k,

7

Le implementazioni ingenue svelerebbero davvero questo problema: quando crei una nuova struttura di dati invece di aggiornarne una esistente sul posto, devi avere un certo sovraccarico.

Lingue diverse hanno modi diversi di affrontarlo e ci sono alcuni trucchi che la maggior parte di loro usa.

Una strategia è la raccolta dei rifiuti . Nel momento in cui la nuova struttura è stata creata, o poco dopo, i riferimenti alla vecchia struttura non rientrano nell'ambito di applicazione e il garbage collector la raccoglierà all'istante o abbastanza presto, a seconda dell'algoritmo GC. Ciò significa che, sebbene vi sia ancora un sovraccarico, è solo temporaneo e non crescerà in modo lineare con la quantità di dati.

Un altro è scegliere diversi tipi di strutture di dati. Laddove le matrici sono la struttura dei dati degli elenchi go-to in linguaggi imperativi (generalmente racchiusi in una sorta di contenitore di riallocazione dinamica come std::vectorin C ++), i linguaggi funzionali spesso preferiscono elenchi collegati. Con un elenco collegato, un'operazione di prepend ('contro') può riutilizzare l'elenco esistente come coda del nuovo elenco, quindi tutto ciò che viene realmente assegnato è il nuovo head dell'elenco. Strategie simili esistono per altri tipi di strutture di dati: insiemi, alberi, e basta.

E poi c'è una valutazione pigra, alla Haskell. L'idea è che le strutture di dati che crei non siano completamente create immediatamente; invece, sono memorizzati come "thunk" (puoi pensare a questi come ricette per costruire il valore quando è necessario). Solo quando è necessario il valore, il thunk viene espanso in un valore reale. Ciò significa che l'allocazione di memoria può essere rinviata fino a quando non è necessaria la valutazione e, a quel punto, è possibile combinare più thunk in una allocazione di memoria.


Caspita, una piccola risposta e tante informazioni / approfondimenti. Grazie :)
Gerry,

3

Conosco solo un po 'di Clojure ed è Immutable Data Structures .

Clojure fornisce una serie di elenchi, vettori, insiemi e mappe immutabili. Dal momento che non possono essere modificati, "aggiungere" o "rimuovere" qualcosa da una collezione immutabile significa creare una nuova collezione proprio come quella vecchia ma con le modifiche necessarie. Persistenza è un termine usato per descrivere la proprietà in cui la vecchia versione della raccolta è ancora disponibile dopo il "cambiamento" e che la raccolta mantiene le sue garanzie di prestazione per la maggior parte delle operazioni. In particolare, ciò significa che la nuova versione non può essere creata utilizzando una copia completa, poiché ciò richiederebbe un tempo lineare. Inevitabilmente, le raccolte persistenti vengono implementate utilizzando strutture di dati collegate, in modo che le nuove versioni possano condividere la struttura con la versione precedente.

Graficamente, possiamo rappresentare qualcosa del genere:

(def my-list '(1 2 3))

    +---+      +---+      +---+
    | 1 | ---> | 2 | ---> | 3 |
    +---+      +---+      +---+

(def new-list (conj my-list 0))

              +-----------------------------+
    +---+     | +---+      +---+      +---+ |
    | 0 | --->| | 1 | ---> | 2 | ---> | 3 | |
    +---+     | +---+      +---+      +---+ |
              +-----------------------------+

2

Oltre a quanto detto in altre risposte, vorrei menzionare il linguaggio di programmazione Clean, che supporta i cosiddetti tipi unici . Non conosco questo linguaggio ma suppongo che tipi unici supportino una sorta di "aggiornamento distruttivo".

In altre parole, mentre la semantica dell'aggiornamento di uno stato è che si crea un nuovo valore da uno vecchio applicando una funzione, il vincolo di unicità può consentire al compilatore di riutilizzare gli oggetti dati internamente perché sa che al vecchio valore non verrà fatto riferimento non più nel programma dopo che è stato prodotto il nuovo valore.

Per maggiori dettagli, vedi ad esempio la home page Clean e questo articolo di Wikipedia

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.