Funzioni pure: "Nessun effetto collaterale" implica "Sempre lo stesso output, dato lo stesso input"?


84

Le due condizioni che definiscono una funzione puresono le seguenti:

  1. Nessun effetto collaterale (ovvero sono consentite solo modifiche all'ambito locale)
  2. Restituisce sempre lo stesso output, dato lo stesso input

Se la prima condizione è sempre vera, ci sono volte in cui la seconda condizione non è vera?

Cioè è davvero necessario solo con la prima condizione?


3
Le tue premesse sono mal specificate. "Input" è troppo ampio. Si può pensare che due funzioni abbiano tipi di input. I loro argomenti e "ambientale" / "contestuale". Una funzione che restituisce l'ora di sistema potrebbe essere considerata pura (anche se ovviamente non lo è) se non si distingue tra questi due tipi di input.
Alexander

4
@Alexander: Nel contesto di "pura funzione", "input" è comunemente inteso come i parametri / argomenti che vengono passati esplicitamente (da qualunque meccanismo utilizzi il linguaggio di programmazione). Fa parte della definizione di "funzione pura". Ma hai ragione, è importante badare alla definizione.
sleske

3
Banale controesempio: restituisce il valore di una variabile globale. Nessun effetto collaterale (il globale viene letto sempre e solo!), Ma risultati potenzialmente diversi ogni volta. (Se non ti piacciono le variabili globali, restituisci l'indirizzo di una variabile locale che dipende dallo stack di chiamate in fase di esecuzione).
Peter - Ripristina Monica il

2
È necessario espandere la propria definizione di "effetti collaterali"; dici che un metodo puro non produce effetti collaterali, ma devi anche notare che un metodo puro non consuma effetti collaterali prodotti altrove.
Eric Lippert

2
@sleske Forse comunemente inteso, ma la mancanza di questa distinzione è la causa esatta della confusione di OP.
Alexander il

Risposte:


114

Ecco alcuni controesempi che non modificano l'ambito esterno ma sono comunque considerati impuri:

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); } (che certamente cambia il PRNG, ma non è considerato osservabile)

L'accesso a variabili non locali non costanti è sufficiente per poter violare la seconda condizione.

Penso sempre alle due condizioni per la purezza come complementari:

  • la valutazione del risultato non deve avere effetti sullo stato collaterale
  • il risultato della valutazione non deve essere influenzato dallo stato laterale

Il termine effetto collaterale si riferisce solo al primo, la funzione che modifica lo stato non locale. Tuttavia, a volte anche le operazioni di lettura sono considerate effetti collaterali: quando sono operazioni e implicano anche la scrittura, anche se il loro scopo principale è accedere a un valore. Esempi di ciò sono la generazione di un numero pseudocasuale che modifica lo stato interno del generatore, la lettura da un flusso di input che fa avanzare la posizione di lettura o la lettura da un sensore esterno che implica un comando di "misura".


1
Grazie Bergi. Per qualche ragione ho pensato che gli effetti collaterali includessero la lettura di variabili al di fuori dell'ambito locale, ma immagino che sia solo un effetto collaterale se scrive tali variabili esterne.
Magnus

17
Se prompt("you choose")non ha effetti collaterali, dovremmo fare un passo indietro e chiarire il significato degli effetti collaterali.
Holger

1
@ Magnus Sì, è esattamente questo che significa effetto . Proverò a chiarire anche nella mia risposta, non mi aspettavo una così grande attenzione e voglio rendere la risposta degna di decine di voti :-)
Bergi

2
Per quanto ne sai, Math.random () restituisce un diodo termico. In realtà non è specificato per utilizzare un cattivo RNG.
Joshua

1
Delle due condizioni, ho sentito la prima chiamata "effetti" mentre la seconda si chiama "coeffetti". Entrambi sono "effetti collaterali" e impuri. f (coeffects, input) -> effects, output I coeffects sono input che provengono dai cambiamenti nell'ambiente più ampio, gli effetti sono output che cambiano l'ambiente più ampio. Elm e Clojurescrips re-frame funzionano con questo modello, per esempio.

30

Il modo "normale" di esprimere cosa sia una funzione pura è in termini di trasparenza referenziale . Una funzione è pura se è referenzialmente trasparente .

Trasparenza referenziale , grosso modo, significa che puoi sostituire la chiamata alla funzione con il suo valore di ritorno o viceversa in qualsiasi punto del programma, senza cambiare il significato del programma.

Quindi, ad esempio, se i C printffossero referenzialmente trasparenti, questi due programmi dovrebbero avere lo stesso significato:

printf("Hello");

e

5;

e tutti i seguenti programmi dovrebbero avere lo stesso significato:

5 + 5;

printf("Hello") + 5;

printf("Hello") + printf("Hello");

Perché printf restituisce il numero di caratteri scritti, in questo caso 5.

Diventa ancora più ovvio con le voidfunzioni. Se ho una funzione void foo, allora

foo(bar, baz, quux);

dovrebbe essere lo stesso di

;

Cioè poiché foonon restituisce nulla, dovrei essere in grado di sostituirlo con nulla senza cambiare il significato del programma.

È chiaro, quindi, che né printffoosono referenzialmente trasparenti, e quindi nessuno dei due è puro. Infatti, una voidfunzione non può mai essere referenzialmente trasparente, a meno che non sia una no-op.

Trovo questa definizione molto più facile da gestire come quella che hai dato. Permette anche di applicarlo a qualsiasi granularità tu voglia: puoi applicarlo a singole espressioni, a funzioni, a interi programmi. Ti permette, ad esempio, di parlare di una funzione come questa:

func fib(n):
    return memo[n] if memo.has_key?(n)
    return 1 if n <= 1
    return memo[n] = fib(n-1) + fib(n-2)

Possiamo analizzare le espressioni che compongono la funzione e concludere facilmente che non sono referenzialmente trasparenti e quindi non pure, poiché utilizzano una struttura dati mutabile, ovvero l' memoarray. Tuttavia, possiamo anche esaminare la funzione e vedere che è referenzialmente trasparente e quindi pura. Questa è talvolta chiamata purezza esterna , cioè una funzione che appare pura al mondo esterno, ma è implementata internamente impura.

Tali funzioni sono comunque utili, perché mentre l'impurità infetta tutto ciò che la circonda, l'interfaccia pura esterna costruisce una sorta di "barriera di purezza", dove l'impurità infetta solo le tre linee della funzione, ma non trapela nel resto del programma . Queste tre righe sono molto più facili da analizzare per la correttezza rispetto all'intero programma.


2
Questa impurità colpisce l'intero programma una volta che hai la concorrenza.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

@R .. Riesci a pensare a un modo in cui la concorrenza potrebbe rendere la funzione di Fibonacci descritta esternamente impura? Non posso. Scrivere su memo[n]è idempotente e non riuscire a leggere da esso spreca semplicemente i cicli della CPU.
Brilliand

Sono d'accordo con entrambi. L'impurità può portare a problemi di concorrenza, ma non in questo caso specifico.
Jörg W Mittag

@R .. Non è difficile immaginare una versione compatibile con la concorrenza.
user253751

1
@Brilliand Ad esempio, memo[n] = ...può prima creare una voce di dizionario e quindi memorizzare il valore in essa. Ciò lascia una finestra durante la quale un altro thread potrebbe vedere una voce non inizializzata.
user253751

12

Mi sembra che la seconda condizione che hai descritto sia un vincolo più debole della prima.

Faccio un esempio, supponiamo di avere una funzione per aggiungerne uno che registri anche alla console:

function addOneAndLog(x) {
  console.log(x);
  return x + 1;
}

La seconda condizione che hai fornito è soddisfatta: questa funzione restituisce sempre lo stesso output quando viene fornito lo stesso input. Tuttavia, non è una funzione pura perché include l'effetto collaterale della registrazione alla console.

Una funzione pura è, in senso stretto, una funzione che soddisfa la proprietà della trasparenza referenziale . Questa è la proprietà che possiamo sostituire un'applicazione di funzione con il valore che produce senza modificare il comportamento del programma.

Supponiamo di avere una funzione che aggiunge semplicemente:

function addOne(x) {
  return x + 1;
}

Possiamo sostituire addOne(5)con 6qualsiasi parte del nostro programma e nulla cambierà.

Al contrario, non possiamo sostituire addOneAndLog(x)con il valore 6ovunque nel nostro programma senza modificare il comportamento perché la prima espressione risulta in qualcosa che viene scritto sulla console mentre la seconda no.

Consideriamo qualsiasi di questo comportamento aggiuntivo che si comporta addOneAndLog(x)oltre alla restituzione dell'output come un effetto collaterale .


"Mi sembra che la seconda condizione che hai descritto sia un vincolo più debole della prima." No, le due condizioni sono logicamente indipendenti.
sleske

@sleske ti sbagli. Ho fornito definizioni chiare per i termini puro ed effetto collaterale. All'interno di questi vincoli, non c'è niente che una funzione senza effetti collaterali oltre a restituire lo stesso output per un dato input. Ho tuttavia fornito esempi in cui la seconda condizione può essere soddisfatta senza la prima. Il concetto fondamentale per comprendere la nozione di purezza è la trasparenza referenziale.
TheInnerLight

Piccolo errore di battitura: non c'è niente che una funzione senza effetti collaterali possa fare oltre a restituire lo stesso output per un dato input.
TheInnerLight

Che ne dici di qualcosa come restituire l'ora corrente? Ciò non ha effetti collaterali, ma restituisce un output diverso per lo stesso input. O più in generale, qualsiasi funzione il cui risultato dipende non solo dai parametri di input, ma anche da una variabile globale (modificabile).
sleske

2
Sembra che tu stia usando una definizione di "effetto collaterale" diversa da quella comunemente usata. Un effetto collaterale è comunemente definito come "un effetto osservabile oltre a restituire un valore" o un "cambiamento di stato osservabile" - vedi ad esempio Wikipedia , questo post su softwareengineering.SE . Hai perfettamente ragione che Date.now()non è puro / referenzialmente trasparente, ma non perché abbia effetti collaterali, ma perché il suo risultato dipende da qualcosa di più del suo input.
sleske

7

Potrebbe esserci una fonte di casualità dall'esterno del sistema. Supponiamo che parte del calcolo includa la temperatura ambiente. Quindi l'esecuzione della funzione produrrà risultati ogni volta diversi a seconda dell'elemento esterno casuale della temperatura ambiente. Lo stato non viene modificato eseguendo il programma.

Tutto quello a cui riesco a pensare, comunque.


3
Secondo me, queste "casualità dall'esterno del sistema" sono una forma di effetto collaterale. Le funzioni con questi comportamenti non sono "pure".
Joseph M. Dion

2

Il problema con le definizioni FP è che sono molto artificiali. Ogni valutazione / calcolo ha effetti collaterali sul valutatore. È teoricamente vero. Negare ciò mostra solo che gli apologeti della FP ignorano la filosofia e la logica: una "valutazione" significa cambiare lo stato di un ambiente intelligente (macchina, cervello, ecc.). Questa è la natura del processo di valutazione. Nessun cambiamento - nessun "calcolo". L'effetto può essere molto visibile: il riscaldamento della CPU o il suo guasto, lo spegnimento della scheda madre in caso di surriscaldamento e così via.

Quando parli di trasparenza referenziale, dovresti capire che le informazioni su tale trasparenza sono disponibili per l'essere umano come creatore dell'intero sistema e detentore di informazioni semantiche e potrebbero non essere disponibili per il compilatore. Ad esempio, una funzione può leggere una risorsa esterna e avrà una monade IO nella sua firma ma restituirà sempre lo stesso valore (ad esempio, il risultato di current_year > 0). Il compilatore non sa che la funzione restituirà sempre lo stesso risultato, quindi la funzione è impura ma ha proprietà referenzialmente trasparenti e può essere sostituita con Truecostante.

Quindi, per evitare tale imprecisione, dovremmo distinguere le funzioni matematiche e le "funzioni" nei linguaggi di programmazione. Le funzioni in Haskell sono sempre impure e la definizione di purezza ad esse correlata è sempre molto condizionale: funzionano su hardware reale con effetti collaterali reali e proprietà fisiche, il che è sbagliato per le funzioni matematiche. Ciò significa che l'esempio con la funzione "printf" è totalmente errato.

Ma non tutte le funzioni matematiche sono pure pure: ogni funzione che ha t(tempo) come parametro può essere impura: tcontiene tutti gli effetti e la natura stocastica della funzione: nel caso comune si ha un segnale in ingresso e non si ha idea dei valori effettivi, può essere anche un rumore.


2

Se la prima condizione è sempre vera, ci sono volte in cui la seconda condizione non è vera?

Considera un semplice snippet di codice di seguito

public int Sum(int a, int b) {
    Random rnd = new Random();
    return rnd.Next(1, 10);
}

Questo codice restituirà un output casuale per lo stesso set di input, tuttavia non ha alcun effetto collaterale.

L'effetto complessivo di entrambi i punti # 1 e # 2 che hai menzionato quando combinati insieme significa: In qualsiasi momento se la funzione Sumcon lo stesso i / p viene sostituita con il suo risultato in un programma, il significato generale del programma non cambia . Questo non è altro che trasparenza referenziale .


Ma in questo caso, la prima condizione non è verificata: la scrittura sulla console è considerata un effetto collaterale, poiché modifica lo stato della macchina stessa.
Gamba destra

@ Rightleg thx per averlo fatto notare. In qualche modo ho frainteso OP in modo totalmente diverso. risposta corretta.
rahulaga_dev

2
Non cambia lo stato del generatore casuale?
Eric Duminil

1
La generazione di un numero casuale è di per sé un effetto collaterale, a meno che lo stato del generatore di numeri casuali non sia fornito esplicitamente, il che farebbe sì che la funzione soddisfi la condizione 2.
TheInnerLight

1
rndnon sfugge alla funzione, quindi il fatto che il suo stato cambi non ha importanza per la purezza della funzione, ma il fatto che il Randomcostruttore utilizzi l'ora corrente come valore seed significa che ci sono "input" diversi da ae b.
Sneftel
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.