Efficienza della programmazione puramente funzionale


397

Qualcuno sa qual è il peggior rallentamento asintotico possibile che può accadere quando si programma in modo puramente funzionale anziché imperativo (cioè consentendo effetti collaterali)?

Chiarimento dal commento di itowlson : c'è qualche problema per il quale l'algoritmo non distruttivo più noto è asintoticamente peggiore dell'algoritmo distruttivo più noto, e se sì di quanto?


6
Lo stesso che quando si programma imperativamente, qualunque cosa sia.
R. Martinho Fernandes,

3
@jldupont: ovviamente per restituire il risultato di un calcolo. Esistono molti programmi gratuiti senza effetti collaterali. Non possono fare molto altro che calcolare sul loro input. Ma è ancora utile.
jalf

24
Posso renderlo cattivo come vuoi, scrivendo male il mio codice funzionale! * ghigno * Penso che quello che stai chiedendo sia "c'è qualche problema per il quale l'algoritmo non distruttivo più noto è asintoticamente peggiore dell'algoritmo distruttivo più noto, e se sì di quanto?" ... è giusto? ?
Itowlson,

2
potresti dare un esempio del tipo di rallentamento che ti interessa. la tua domanda è un po 'vaga.
Peter Recore,

5
Un utente ha eliminato la sua risposta, ma ha affermato che la versione funzionale del problema delle 8 regine durava più di un minuto per n = 13. Ha ammesso che non era "scritto molto bene", quindi ho deciso di scrivere la mia versione del 8 regine in F #: pastebin.com/ffa8d4c4 . Inutile dire che il mio programma puramente funzionale calcola n = 20 in poco più di un secondo.
Juliet,

Risposte:


531

Secondo Pippenger [1996] , quando si confronta un sistema Lisp che è puramente funzionale (e ha una semantica di valutazione rigorosa, non pigra) con uno che può mutare i dati, un algoritmo scritto per l'implicito Lisp che gira in O ( n ) può essere tradotto ad un algoritmo nel puro Lisp che gira in tempo O ( n log n ) (basato sul lavoro di Ben-Amram e Galil [1992] sulla simulazione della memoria ad accesso casuale usando solo puntatori). Pippenger stabilisce anche che esistono algoritmi per i quali è il meglio che puoi fare; ci sono problemi che sono O ( n ) nel sistema impuro che sono Ω ( n log n ) nel sistema puro.

Ci sono alcuni avvertimenti su questo documento. Il più significativo è che non si rivolge a linguaggi funzionali pigri, come Haskell. Bird, Jones e De Moor [1997] dimostrano che il problema costruito da Pippenger può essere risolto in un linguaggio funzionale pigro in O ( n ) tempo, ma non stabiliscono (e per quanto ne so, nessuno ha) se o non un linguaggio funzionale pigro può risolvere tutti i problemi nello stesso tempo di esecuzione asintotico di un linguaggio con mutazione.

Il problema creato da Pippenger richiede che Ω ( n log n ) sia specificamente progettato per ottenere questo risultato e non è necessariamente rappresentativo di problemi pratici del mondo reale. Ci sono alcune restrizioni al problema che sono un po 'inaspettate, ma necessarie affinché la prova funzioni; in particolare, il problema richiede che i risultati vengano calcolati online, senza poter accedere a input futuri e che l'input sia costituito da una sequenza di atomi da un insieme illimitato di possibili atomi, piuttosto che da un set di dimensioni fisse. E il documento stabilisce solo risultati (limite inferiore) per un algoritmo impuro di tempo di esecuzione lineare; per problemi che richiedono un tempo di esecuzione maggiore, è possibile che la O aggiuntiva (log n) il fattore riscontrato nel problema lineare può essere in grado di essere "assorbito" nel processo di operazioni extra necessarie per algoritmi con tempi di esecuzione maggiori. Questi chiarimenti e domande aperte vengono esplorati brevemente da Ben-Amram [1996] .

In pratica, molti algoritmi possono essere implementati in un linguaggio funzionale puro con la stessa efficienza di un linguaggio con strutture di dati mutabili. Per un buon riferimento sulle tecniche da utilizzare per implementare in modo efficiente strutture di dati puramente funzionali, vedere "Strutture di dati puramente funzionali" di Chris Okasaki [Okasaki 1998] (che è una versione estesa della sua tesi [Okasaki 1996] ).

Chiunque abbia bisogno di implementare algoritmi su strutture di dati puramente funzionali dovrebbe leggere Okasaki. Puoi sempre ottenere nel peggiore dei casi un rallentamento O (log n ) per operazione simulando la memoria mutabile con un albero binario bilanciato, ma in molti casi puoi fare molto meglio di così, e Okasaki descrive molte tecniche utili, dalle tecniche ammortizzate a quelle reali- quelli che eseguono il lavoro ammortizzato in modo incrementale. Le strutture di dati puramente funzionali possono essere un po 'difficili da lavorare e analizzare, ma offrono molti vantaggi come la trasparenza referenziale che sono utili nell'ottimizzazione del compilatore, nel calcolo parallelo e distribuito e nell'implementazione di funzionalità come il versioning, l'annullamento e il rollback.

Si noti inoltre che tutto ciò discute solo i tempi di esecuzione asintotici. Molte tecniche per l'implementazione di strutture di dati puramente funzionali forniscono una certa quantità di costante rallentamento dei fattori, a causa della contabilità aggiuntiva necessaria per il loro funzionamento e dei dettagli di implementazione della lingua in questione. I vantaggi di strutture di dati puramente funzionali possono superare questi rallentamenti dei fattori costanti, quindi in genere sarà necessario effettuare dei compromessi in base al problema in questione.

Riferimenti


50
Pippinger è l'autorità indiscussa su questa domanda. Ma dovremmo sottolineare che i suoi risultati sono teorici , non pratici. Quando si tratta di rendere le strutture di dati funzionali pratiche ed efficienti, non si può fare di meglio di Okasaki.
Norman Ramsey,

6
itowlson: Devo ammettere che non ho letto abbastanza di Pippenger per rispondere alla tua domanda; è stato pubblicato in una rivista peer review, citata da Okasaki, e ne ho letto abbastanza per determinare che le sue affermazioni sono pertinenti a questa domanda, ma non abbastanza per comprenderne la prova. L'asporto immediato che ricevo per le conseguenze del mondo reale è che è banale per convertire un O ( n algoritmo) impuro in un O ( n log n ) una pura, semplicemente simulando memoria modificabili tramite un albero binario bilanciato. Ci sono problemi che non possono fare di meglio; Non so se sono puramente teorici.
Brian Campbell,

3
Il risultato di Pippenger fa due importanti presupposti che ne limitano l'ambito: considera i calcoli "on-line" o "reattivi" (non il solito modello di calcolo che associa input finiti a un singolo output) e calcoli "simbolici" in cui gli input sono sequenze di atomi, che possono essere testati solo per l'uguaglianza (cioè, l'interpretazione dell'input è estremamente primitiva).
Chris Conway,

2
Ottima risposta; Vorrei aggiungere che per linguaggi puramente funzionali non esiste un modello universalmente concordato per la complessità informatica, mentre nel mondo impuro la macchina RAM a costo unitario è relativamente standard (quindi questo rende il confronto più difficile). Si noti inoltre che il limite superiore di una differenza Lg (N) in puro / impuro può essere intuitivamente spiegato molto facilmente osservando un'implementazione di array in un linguaggio puro (costa lg (n) per operazione (e si ottiene la cronologia)) .
user51568,

4
Punto importante: tradurre scrupolosamente una specifica puramente funzionale in un'implementazione puramente funzionale più complicata ed efficiente è di scarso vantaggio se alla fine, automaticamente o manualmente, la tradurrà in un codice impuro ancora più efficiente. L'impurità non è un grosso problema se riesci a tenerlo in una gabbia, ad esempio bloccandolo in una funzione priva di effetti collaterali esterni.
Robin Green,

44

Esistono infatti diversi algoritmi e strutture di dati per i quali non è nota alcuna soluzione puramente funzionale asintoticamente efficiente (ti one implementabile in puro calcolo lambda), anche con pigrizia.

  • Il summenzionato ritrovamento sindacale
  • Tabelle hash
  • Array
  • Alcuni algoritmi grafici
  • ...

Tuttavia, supponiamo che nelle lingue "imperative" l'accesso alla memoria sia O (1) mentre in teoria non può essere così asintoticamente (cioè per dimensioni di problemi illimitate) e l'accesso alla memoria all'interno di un enorme set di dati è sempre O (log n) , che può essere emulato in un linguaggio funzionale.

Inoltre, dobbiamo ricordare che in realtà tutti i linguaggi funzionali moderni forniscono dati mutabili e Haskell li fornisce anche senza sacrificare la purezza (la monade ST).


3
Se il set di dati si adatta alla memoria fisica, l'accesso è O (1) in quanto è possibile trovare un limite superiore assoluto sulla quantità di tempo per leggere qualsiasi elemento. Se il set di dati non funziona, stai parlando di I / O e questo sarà di gran lunga il fattore dominante, tuttavia il programma è stato scritto.
Donal Fellows,

Bene, ovviamente sto parlando di O (log n) operazioni di accesso alla memoria esterna. Comunque, in ogni caso stavo parlando bs: la memoria esterna può anche essere O (1) -dressable ...
jkff

2
Penso che una delle cose più grandi che la programmazione imperativa guadagna rispetto alla programmazione funzionale sia la capacità di contenere riferimenti a molti aspetti distinti di uno stato e generare un nuovo stato in modo tale che tutti quei riferimenti rimandino agli aspetti corrispondenti del nuovo stato. L'uso della programmazione funzionale richiederebbe la sostituzione delle operazioni di dereferenziamento diretto con operazioni di ricerca per trovare l'aspetto appropriato di una particolare versione dell'attuale stato generale.
supercat

Anche il modello di puntatore (O (log n) accesso alla memoria, parlando in senso lato) non è fisicamente realistico su scale estremamente grandi. La velocità della luce limita la rapidità con cui diversi dispositivi informatici possono comunicare tra loro, mentre attualmente si ritiene che la massima quantità di informazioni che possono essere conservate in una determinata regione sia limitata dalla sua superficie.
dfeuer,

36

Questo articolo afferma che le note implementazioni puramente funzionali dell'algoritmo union-find hanno tutte una complessità asintotica peggiore di quella che pubblicano, che ha un'interfaccia puramente funzionale ma utilizza internamente dati mutabili.

Il fatto che altre risposte affermino che non ci può mai essere alcuna differenza e che, ad esempio, l'unico "inconveniente" del codice puramente funzionale è che può essere parallelizzato ti dà un'idea dell'informalità / obiettività della comunità di programmazione funzionale su questi argomenti .

MODIFICARE:

I commenti che seguono sottolineano che una discussione parziale dei pro e dei contro della pura programmazione funzionale potrebbe non provenire dalla "comunità di programmazione funzionale". Buon punto. Forse i sostenitori che vedo sono giusti, per citare un commento, "analfabeti".

Ad esempio, penso che questo post sul blog sia stato scritto da qualcuno che potrebbe essere considerato rappresentativo della comunità di programmazione funzionale, e dato che è un elenco di "punti per una valutazione pigra", sarebbe un buon posto per menzionare qualsiasi inconveniente che potrebbe essere una programmazione pigra e puramente funzionale. Un buon posto sarebbe stato al posto del seguente licenziamento (tecnicamente vero, ma distorto al punto da non essere divertente):

Se una funzione rigorosa ha una complessità O (f (n)) in un linguaggio rigoroso, allora ha anche una complessità O (f (n)) in un linguaggio pigro. Perché preoccuparsi? :)


4

Con un limite superiore fisso sull'utilizzo della memoria, non ci dovrebbero essere differenze.

Schizzo di prova: dato un limite superiore fisso sull'utilizzo della memoria, si dovrebbe essere in grado di scrivere una macchina virtuale che esegue un set di istruzioni imperativo con la stessa complessità asintotica come se si stesse eseguendo effettivamente su quella macchina. Questo perché è possibile gestire la memoria mutabile come una struttura di dati persistente, fornendo O (log (n)) in lettura e scrittura, ma con un limite superiore fisso sull'utilizzo della memoria, è possibile avere una quantità fissa di memoria, causando decadimento in O (1). Pertanto l'implementazione funzionale può essere la versione imperativa in esecuzione nell'implementazione funzionale della VM e quindi dovrebbero avere entrambe la stessa complessità asintotica.


6
Un limite superiore fisso sull'utilizzo della memoria non è il modo in cui le persone analizzano questo tipo di cose; si assume una memoria arbitrariamente grande, ma finita. Quando implemento un algoritmo, sono interessato a come scalerà dall'input più semplice fino a qualsiasi dimensione di input arbitraria. Se si imposta un limite superiore fisso sull'utilizzo della memoria, perché non si mette anche un limite superiore fisso su quanto tempo si impiegherà il calcolo e si dice che tutto è O (1)?
Brian Campbell,

@Brian Campbell: è vero. Sto solo suggerendo che se lo volessi, potresti ignorare la differenza nel fattore costante in molti casi nella pratica. Uno dovrebbe ancora essere consapevole della differenza quando si scende a compromessi tra memoria e tempo per assicurarsi che l'uso di m volte più memoria riduca il tempo di esecuzione di almeno un fattore di log (m).
Brian

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.