Una funzione pura memorizzata stessa è considerata pura?


47

Diciamo che fn(x)è una funzione pura che fa qualcosa di costoso, come restituire un elenco dei fattori primi di x.

E diciamo che creiamo una versione memorizzata della stessa funzione chiamata memoizedFn(x). Restituisce sempre lo stesso risultato per un determinato input, ma mantiene una cache privata dei risultati precedenti per migliorare le prestazioni.

Formalmente parlando, è memoizedFn(x)considerato puro?

Oppure c'è qualche altro nome o termine qualificativo usato per riferirsi a tale funzione nelle discussioni sul PQ? (ovvero una funzione con effetti collaterali che può influire sulla complessità computazionale delle chiamate successive ma che non può influire sui valori di ritorno.)


24
Forse non è puro per i puristi, ma "abbastanza puro" per le persone pragmatiche ;-)
Doc Brown,

2
@DocBrown Sono d'accordo, mi chiedo solo se esiste un termine più formale per "abbastanza puro"
callum

13
L'esecuzione di una funzione pura modificherà molto probabilmente la cache delle istruzioni del processore, i predittori di rami, ecc. Ma questo è probabilmente "abbastanza puro" anche per i puristi - oppure puoi dimenticare completamente le funzioni pure.
gnasher729,

10
@callum No, non esiste una definizione formale di "abbastanza puro". Quando si discute della purezza e dell'equivalenza semantica di due chiamate "referenzialmente trasparenti", è sempre necessario indicare esattamente quale semantica si intende applicare. A un livello basso di dettagli di implementazione, si romperà sempre e avrà diversi effetti o tempi di memoria. Ecco perché devi essere pragmatico: quale livello di dettaglio è utile per ragionare sul tuo codice?
Bergi,

3
Quindi, per motivi di pragmatismo, direi che la purezza dipende dal fatto che consideri o meno il tempo di calcolo come parte dell'output. funcx(){sleep(cached_time--); return 0;}restituisce la stessa val ogni volta, ma si esibirà diversamente
Mars

Risposte:


41

Sì. Anche la versione memorizzata di una funzione pura è una funzione pura.

Tutto ciò che interessa alla purezza della funzione è l'effetto che i parametri di input sul valore di ritorno della funzione (passare lo stesso input dovrebbe sempre produrre lo stesso output) e tutti gli effetti collaterali rilevanti per gli stati globali (ad es. Testo al terminale o interfaccia utente o rete) . Il tempo di calcolo e gli usi di memoria extra sono irrilevanti per il funzionamento della purezza.

Le cache di una funzione pura sono praticamente invisibili al programma; un linguaggio di programmazione funzionale è autorizzato ad ottimizzare automaticamente una funzione pura su una versione memorizzata della funzione se può determinare che sarà vantaggioso farlo. In pratica, determinare automaticamente quando la memoizzazione è utile è in realtà un problema piuttosto difficile, ma tale ottimizzazione sarebbe valida.


19

Wikipedia definisce una "Funzione pura" come una funzione che ha le seguenti proprietà:

  • Il valore restituito è lo stesso per gli stessi argomenti (nessuna variazione con variabili statiche locali, variabili non locali, argomenti di riferimento mutabili o flussi di input da dispositivi I / O).

  • La sua valutazione non ha effetti collaterali (nessuna mutazione di variabili statiche locali, variabili non locali, argomenti di riferimento mutabili o flussi I / O).

In effetti, una funzione pura restituisce lo stesso output dato lo stesso input e non influisce su nessun altro al di fuori della funzione. Ai fini della purezza, non importa come la funzione calcola il suo valore di ritorno, purché restituisca lo stesso output dato lo stesso input.

Linguaggi funzionalmente puri come Haskell usano abitualmente la memoizzazione per accelerare una funzione memorizzando nella cache i risultati precedentemente calcolati.


16
Potrei perdere qualcosa, ma come hai intenzione di mantenere la cache senza effetti collaterali?
val

1
Tenendolo all'interno della funzione.
Robert Harvey,

4
"nessuna mutazione della variabile statica locale" sembra escludere anche le variabili locali persistenti tra le chiamate.
val

3
Questo in realtà non risponde alla domanda, anche se sembra implicare che sì, è puro.
Marte

6
@val Hai ragione: questa condizione deve essere leggermente rilassata. La memoizzazione puramente funzionale a cui si riferisce non ha una mutazione visibile di alcun dato statico. Quello che succede è che il risultato viene quindi calcolato e memorizzato la prima volta che viene chiamata la funzione e restituisce lo stesso valore ogni volta che viene chiamato. Molte lingue hanno un idioma per questo: una static constvariabile locale in C ++ (ma non C) o una struttura di dati valutata pigramente in Haskell. C'è un'altra condizione che ti serve: l'inizializzazione deve essere thread-safe.
Davislor,

7

Sì, le funzioni pure memorizzate vengono comunemente chiamate pure. Ciò è particolarmente comune in lingue come Haskell, in cui i risultati memorizzati, valutati pigramente e immutabili sono una funzionalità integrata.

C'è un avvertimento importante: la funzione di memoizing deve essere thread-safe, altrimenti potresti ottenere una condizione di competizione quando due thread tentano entrambi di chiamarla.

Un esempio di un informatico che usa il termine "puramente funzionale" in questo modo è questo post sul blog di Conal Elliott sulla memorizzazione automatica:

Forse sorprendentemente, la memoizzazione può essere implementata in modo semplice e puramente funzionale in un linguaggio funzionale pigro.

Ci sono molti esempi nella letteratura peer-reviewed e sono stati per decenni. Ad esempio, questo documento del 1995, "Utilizzo della memorizzazione automatica come strumento di ingegneria del software nei sistemi di intelligenza artificiale del mondo reale", utilizza un linguaggio molto simile nella sezione 5.2 per descrivere quella che oggi chiameremmo una pura funzione:

La memorizzazione funziona solo per funzioni vere, non per procedure. Cioè, se il risultato di una funzione non è completamente e deterministicamente specificato dai suoi parametri di input, l'utilizzo della memoization darà risultati errati. Il numero di funzioni che possono essere memorizzate con successo verrà aumentato incoraggiando l'uso di uno stile di programmazione funzionale in tutto il sistema.

Alcune lingue imperative hanno un linguaggio simile. Ad esempio, una static constvariabile in C ++ viene inizializzata solo una volta, prima che venga utilizzato il suo valore e non muta mai.


3

Dipende da come lo fai.

Di solito le persone vogliono memoize mutando una sorta di dizionario della cache. Ciò ha tutti i problemi associati alla mutazione impura, come doversi preoccupare della concorrenza, preoccuparsi che la cache diventi troppo grande, ecc.

Tuttavia, è possibile memoize senza mutazione della memoria impura. Un esempio è in questa risposta , in cui seguo i valori memorizzati esternamente per mezzo di un lengthsargomento.

Nel link fornito da Robert Harvey , la valutazione pigra viene utilizzata per evitare effetti collaterali.

Un'altra tecnica talvolta vista è quella di contrassegnare esplicitamente la memoizzazione come un effetto collaterale impuro nel contesto di un IOtipo, ad esempio con la funzione di memoize dell'effetto gatti .

Quest'ultimo sottolinea il fatto che a volte l'obiettivo è solo incapsulare la mutazione piuttosto che eliminarla. Molti programmatori funzionali lo considerano "abbastanza puro" per rendere esplicita e incapsulata l'impurità.

Se vuoi un termine per differenziarlo da una funzione veramente pura, penso che sia sufficiente dire "memorizzato con un dizionario mutabile". Ciò consente alle persone di sapere come usarlo in sicurezza.


Non credo che nessuna delle soluzioni più pure risolva i problemi di cui sopra: mentre perdi qualsiasi preoccupazione di concorrenza, perdi anche qualsiasi possibilità per due chiamate avviate contemporaneamente come collatz(100)e collatz(200)per cooperare. E IIUIC, il problema con la cache che diventa troppo grande rimane (anche se Haskell potrebbe avere qualche bel trucco per questo?).
Maaartinus,

Nota: IOè puro. Tutti i metodi impuri su IOe Cats sono nominati unsafe. Async.memoizeè anche puro, quindi non dobbiamo accontentarci di "abbastanza puro" :)
Samuel

2

Di solito, una funzione che restituisce un elenco non è affatto pura perché richiede un'allocazione di memoria e può quindi fallire (ad esempio generando un'eccezione, che non è pura). Una lingua che ha tipi di valore e può rappresentare un elenco come tipo di valore di dimensioni limitate potrebbe non presentare questo problema. Per questo motivo, il tuo esempio probabilmente non è puro.

In generale, se la memoizzazione può essere eseguita in modo da evitare errori (ad es. Avendo una memoria allocata staticamente per i risultati memorizzati e la sincronizzazione interna per controllare l'accesso ad essi se la lingua ammette i thread), è ragionevole considerare tale funzione puro.


0

Puoi implementare la memoizzazione senza effetti collaterali usando la monade dello stato .

[State monad] è sostanzialmente una funzione S => (S, A), dove S è il tipo che rappresenta il tuo stato e A è il risultato che la funzione produce - Cats State .

Nel tuo caso lo stato sarebbe il valore memorizzato o niente (cioè Haskell Maybeo Scala Option[A]). Se il valore memorizzato è presente, viene restituito come A, altrimenti Aviene calcolato e restituito sia come stato di transizione sia come risultato.

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.