È una funzione pura?


117

La maggior parte delle fonti definisce una funzione pura come avente le seguenti due proprietà:

  1. Il valore restituito è lo stesso per gli stessi argomenti.
  2. La sua valutazione non ha effetti collaterali.

È la prima condizione che mi riguarda. Nella maggior parte dei casi, è facile giudicare. Considera le seguenti funzioni JavaScript (come mostrato in questo articolo )

Puro:

const add = (x, y) => x + y;

add(2, 4); // 6

Impuro:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

È facile vedere che la seconda funzione fornirà uscite diverse per le chiamate successive, violando così la prima condizione. E quindi, è impuro.

Questa parte ho capito.


Ora, per la mia domanda, considera questa funzione che converte un dato importo in dollari in euro:

(MODIFICA - Uso constnella prima riga. Usato in letprecedenza inavvertitamente.)

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Supponiamo di recuperare il tasso di cambio da un db e che cambia ogni giorno.

Ora, non importa quante volte oggi chiamo questa funzione , mi darà lo stesso output per l'input 100. Tuttavia, domani potrebbe darmi un risultato diverso. Non sono sicuro se questo viola o meno la prima condizione.

IOW, la stessa funzione non contiene alcuna logica per mutare l'input, ma si basa su una costante esterna che potrebbe cambiare in futuro. In questo caso, è assolutamente certo che cambierà ogni giorno. In altri casi, potrebbe succedere; potrebbe non farlo.

Possiamo chiamare tali funzioni funzioni pure. Se la risposta è NO, come possiamo quindi riformattarla in una sola?


6
La purezza di un linguaggio così dinamico come JS è un argomento molto complicato:function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
zerkms,

29
Purezza significa che è possibile sostituire la chiamata di funzione con il suo valore di risultato a livello di codice senza modificare il comportamento del programma.
bob

1
Per andare un po 'oltre su ciò che costituisce un effetto collaterale, e con una terminologia più teorica, vedi cs.stackexchange.com/questions/116377/…
smetti di essere malvagio'

3
Oggi la funzione è (x) => {return x * 0.9;}. Domani avrai una funzione diversa che sarà pure pura, forse (x) => {return x * 0.89;}. Si noti che ogni volta che si esegue (x) => {return x * exchangeRate;}crea una nuova funzione e quella funzione è pura perché exchangeRatenon può cambiare.
user253751

2
Questa è una funzione impura, se vuoi renderla pura, puoi usarla const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; per una funzione pura, Its return value is the same for the same arguments.dovrebbe tenere sempre, 1 secondo, 1 decennio .. dopo, qualunque cosa
accada

Risposte:


133

Il dollarToEurovalore di ritorno dipende da una variabile esterna che non è un argomento; pertanto, la funzione è impura.

Nella risposta è NO, come possiamo quindi riformattare la funzione come pura?

Un'opzione è passare exchangeRate. In questo modo, ogni argomenti di si (something, somethingElse), l'uscita è garantito per essere something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Si noti che per la programmazione funzionale, è necessario evitare let: utilizzare sempre constper evitare la riassegnazione.


6
Non avendo variabili libere non è un requisito per una funzione di essere puro: const add = x => y => x + y; const one = add(42);Qui entrambi adde onesono funzioni pure.
zerkms,

7
const foo = 42; const add42 = x => x + foo;<- questa è un'altra funzione pura, che utilizza nuovamente variabili libere.
zerkms,

8
@zerkms - Sarei molto desideroso di vedere la tua risposta a questa domanda (anche se riformulasse solo CertainPerformance per usare una terminologia diversa). Non penso che sarebbe una duplicazione, e sarebbe illuminante, soprattutto se citato (idealmente con fonti migliori rispetto all'articolo di Wikipedia sopra, ma se è tutto ciò che otteniamo, comunque una vittoria). (Sarebbe facile leggere questo commento in una sorta di luce negativa. Fidati di me che sono sincero, penso che una risposta del genere sarebbe fantastica e mi piacerebbe leggerla.)
TJ Crowder

17
Penso che sia tu che @zerkms avete sbagliato. Sembra che tu pensi che la dollarToEurofunzione nell'esempio nella tua risposta sia impura perché dipende dalla variabile libera exchangeRate. È assurdo. Come ha sottolineato zerkms, la purezza di una funzione non ha nulla a che fare con se ha o meno variabili libere. Tuttavia, zerkms ha torto anche perché ritiene che la dollarToEurofunzione sia impura perché dipende da exchangeRatequale proviene da un database. Dice che è impuro perché "dipende transitivamente dall'IO".
Aadit M Shah,

9
(cont) Di nuovo, è assurdo perché suggerisce che dollarToEuroè impuro perché exchangeRateè una variabile libera. Suggerisce che se exchangeRatenon fosse una variabile libera, cioè se fosse un argomento, dollarToEurosarebbe puro. Quindi, suggerisce che dollarToEuro(100)è impuro ma dollarToEuro(100, exchangeRate)è puro. Questo è chiaramente assurdo perché in entrambi i casi dipende da exchangeRatequale proviene da un database. L'unica differenza è se exchangeRateè una variabile libera all'interno della dollarToEurofunzione.
Aadit M Shah,

76

Tecnicamente, qualsiasi programma che esegui su un computer è impuro perché alla fine si compila in istruzioni come "sposta questo valore in eax" e "aggiungi questo valore al contenuto di eax", che sono impure. Non è molto utile.

Invece, pensiamo alla purezza usando le scatole nere . Se un certo codice produce sempre gli stessi output quando vengono dati gli stessi input, viene considerato puro. Con questa definizione, anche la seguente funzione è pura anche se internamente utilizza una tabella di memo impura.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

Non ci preoccupiamo degli interni perché stiamo usando una metodologia della scatola nera per verificare la purezza. Allo stesso modo, non ci interessa che alla fine tutto il codice venga convertito in istruzioni impure della macchina perché stiamo pensando alla purezza usando una metodologia a scatola nera. Gli interni non sono importanti.

Ora, considera la seguente funzione.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

La greetfunzione è pura o impura? Con la nostra metodologia della scatola nera, se gli diamo lo stesso input (ad es. World) Allora stampa sempre lo stesso output sullo schermo (es Hello World!.). In questo senso, non è puro? No non lo è. Il motivo non è puro perché consideriamo la stampa di qualcosa sullo schermo un effetto collaterale. Se la nostra scatola nera produce effetti collaterali, allora non è pura.

Che cos'è un effetto collaterale? È qui che è utile il concetto di trasparenza referenziale . Se una funzione è referenzialmente trasparente, possiamo sempre sostituire le applicazioni di quella funzione con i loro risultati. Si noti che questo non è lo stesso della funzione inline .

Nell'integrazione delle funzioni, sostituiamo le applicazioni di una funzione con il corpo della funzione senza alterare la semantica del programma. Tuttavia, una funzione referenzialmente trasparente può sempre essere sostituita con il suo valore di ritorno senza alterare la semantica del programma. Considera il seguente esempio.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Qui, abbiamo sottolineato la definizione di greete non ha cambiato la semantica del programma.

Ora, considera il seguente programma.

undefined;
undefined;

Qui, abbiamo sostituito le applicazioni della greetfunzione con i loro valori di ritorno e ha cambiato la semantica del programma. Non stampiamo più i saluti sullo schermo. Questo è il motivo per cui la stampa è considerata un effetto collaterale, ed è per questo che la greetfunzione è impura. Non è referenzialmente trasparente.

Consideriamo ora un altro esempio. Considera il seguente programma.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

Chiaramente, la mainfunzione è impura. Tuttavia, la timeDifffunzione è pura o impura? Anche se dipende da serverTimequale proviene da una chiamata di rete impura, è ancora referenzialmente trasparente perché restituisce gli stessi output per gli stessi input e perché non ha effetti collaterali.

zerkms probabilmente non sarà d'accordo con me su questo punto. Nella sua risposta , ha affermato che la dollarToEurofunzione nell'esempio seguente è impura perché "dipende transitoriamente dall'IO".

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Non sono d'accordo con lui perché il fatto che exchangeRateprovenga da un database è irrilevante. È un dettaglio interno e la nostra metodologia della scatola nera per determinare la purezza di una funzione non si preoccupa dei dettagli interni.

In linguaggi puramente funzionali come Haskell, abbiamo un tratteggio di escape per l'esecuzione di effetti IO arbitrari. Si chiama unsafePerformIOe, come suggerisce il nome, se non lo si utilizza correttamente, non è sicuro perché potrebbe violare la trasparenza referenziale. Tuttavia, se sai cosa stai facendo, è perfettamente sicuro da usare.

Viene generalmente utilizzato per il caricamento di dati da file di configurazione vicino all'inizio del programma. Il caricamento di dati da file di configurazione è un'operazione IO impura. Tuttavia, non vogliamo essere gravati dal passaggio dei dati come input per ogni funzione. Quindi, se lo utilizziamo unsafePerformIO, possiamo caricare i dati al livello più alto e tutte le nostre funzioni pure possono dipendere dai dati di configurazione globali immutabili.

Notare che solo perché una funzione dipende da alcuni dati caricati da un file di configurazione, un database o una chiamata di rete, non significa che la funzione sia impura.

Tuttavia, consideriamo il tuo esempio originale che ha una semantica diversa.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Qui, suppongo che poiché exchangeRatenon è definito come const, verrà modificato mentre il programma è in esecuzione. In tal caso, allora dollarToEuroè sicuramente una funzione impura perché quando exchangeRateviene modificata, si romperà la trasparenza referenziale.

Tuttavia, se la exchangeRatevariabile non viene modificata e non verrà mai modificata in futuro (ovvero se è un valore costante), anche se è definita come let, non si romperà la trasparenza referenziale. In tal caso, dollarToEuroè davvero una funzione pura.

Si noti che il valore di exchangeRatepuò cambiare ogni volta che si esegue nuovamente il programma e non romperà la trasparenza referenziale. Rompe la trasparenza referenziale solo se cambia mentre il programma è in esecuzione.

Ad esempio, se esegui il mio timeDiffesempio più volte, otterrai valori diversi serverTimee quindi risultati diversi. Tuttavia, poiché il valore di serverTimenon cambia mai mentre il programma è in esecuzione, la timeDifffunzione è pura.


3
Questo è stato molto istruttivo. Grazie. E intendevo usare constnel mio esempio.
Pupazzo di neve,

3
Se intendevi usare, constla dollarToEurofunzione è davvero pura. L'unico modo in cui exchangeRatecambierebbe il valore di è se avessi eseguito nuovamente il programma. In tal caso, il vecchio processo e il nuovo processo sono diversi. Quindi, non rompe la trasparenza referenziale. È come chiamare una funzione due volte con argomenti diversi. Gli argomenti potrebbero essere diversi ma all'interno della funzione il valore degli argomenti rimane costante.
Aadit M Shah,

3
Questo suona come una piccola teoria sulla relatività: le costanti sono solo relativamente costanti, non assolutamente, relativamente al processo in esecuzione. Chiaramente l'unica risposta giusta qui. +1.
bob

5
Non sono d'accordo con "è impuro perché alla fine si compila in istruzioni come" sposta questo valore in eax "e" aggiungi questo valore al contenuto di eax " . Se eaxviene cancellato - tramite un carico o un clear - il codice rimane deterministico indipendentemente da cos'altro sta succedendo ed è quindi puro. Altrimenti, risposta molto esauriente.
3Dave

3
@Bergi: In realtà, in un linguaggio puro con valori immutabili, l'identità è irrilevante. Se due riferimenti che valutano lo stesso valore sono due riferimenti allo stesso oggetto o a oggetti diversi, si può osservare solo mutando l'oggetto attraverso uno dei riferimenti e osservando se il valore cambia anche quando viene recuperato attraverso l'altro riferimento. Senza mutazione, l'identità diventa irrilevante. (Come direbbe Rich Hickey: L'identità è una serie di Stati nel tempo.)
Jörg W Mittag,

23

Una risposta di un me-purista (dove "io" sono letteralmente io, poiché penso che questa domanda non abbia una sola risposta "giusta" formale ):

In un linguaggio così dinamico come JS con così tante possibilità di scansionare i tipi di base di patch o creare tipi personalizzati usando funzionalità come Object.prototype.valueOfè impossibile dire se una funzione è pura solo guardandola, dal momento che dipende dal chiamante se vogliono per produrre effetti collaterali.

Una demo:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Una risposta di me-pragmatico:

Dalla stessa definizione da Wikipedia

Nella programmazione per computer, una funzione pura è una funzione che ha le seguenti proprietà:

  1. 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).
  2. 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 altre parole, importa solo come si comporta una funzione, non come viene implementata. E fintanto che una particolare funzione contiene queste 2 proprietà: è pura indipendentemente da come sia stata implementata.

Ora alla tua funzione:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

È impuro perché non qualifica il requisito 2: dipende transitivamente dall'IO.

Sono d'accordo che la dichiarazione sopra è sbagliata, vedere l'altra risposta per i dettagli: https://stackoverflow.com/a/58749249/251311

Altre risorse pertinenti:


4
@TJCrowder mecome zerkms che fornisce una risposta.
zerkms,

2
Sì, con Javascript si tratta di sicurezza, non di garanzie
bob

4
@bob ... o è una chiamata bloccante.
zerkms,

1
@zerkms - Grazie. Solo così sono sicuro al 100%, la differenza chiave tra te add42e my addXè puramente che il mio xpuò essere modificato e il tuo ftnon può essere modificato (e quindi add42il valore di ritorno non varia in base a ft)?
TJ Crowder,

5
Non sono d'accordo sul fatto che la dollarToEurofunzione nel tuo esempio sia impura. Ho spiegato perché non sono d'accordo nella mia risposta. stackoverflow.com/a/58749249/783743
Aadit M Shah

14

Come altre risposte hanno detto, il modo in cui hai implementato dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

è davvero puro, perché il tasso di cambio non viene aggiornato mentre il programma è in esecuzione. Concettualmente, tuttavia, dollarToEurosembra che dovrebbe essere una funzione impura, in quanto utilizza qualunque sia il tasso di cambio più aggiornato. Il modo più semplice per spiegare questa discrepanza è che non hai implementato dollarToEuroma dollarToEuroAtInstantOfProgramStart.

La chiave qui è che ci sono diversi parametri che sono necessari per calcolare una conversione di valuta e che una versione veramente pura del generale li dollarToEurofornirebbe tutti. I parametri più diretti sono la quantità di USD da convertire e il tasso di cambio. Tuttavia, poiché desideri ottenere il tasso di cambio dalle informazioni pubblicate, ora hai tre parametri da fornire:

  • La quantità di denaro da scambiare
  • Un'autorità storica da consultare per i tassi di cambio
  • La data in cui è avvenuta la transazione (per indicizzare l'autorità storica)

L'autorità storica qui è il tuo database e supponendo che il database non sia compromesso, restituirà sempre lo stesso risultato per il tasso di cambio in un determinato giorno. Quindi, con la combinazione di questi tre parametri, puoi scrivere una versione completamente pura e autosufficiente del generale dollarToEuro, che potrebbe assomigliare a questa:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

L'implementazione acquisisce valori costanti sia per l'autorità storica sia per la data della transazione nel momento in cui viene creata la funzione: l'autorità storica è il database e la data acquisita è la data di avvio del programma; tutto ciò che rimane è l'importo in dollari , fornito dal chiamante. La versione impura di dollarToEuroquello ottiene sempre il valore più aggiornato essenzialmente prende implicitamente il parametro della data, impostandolo sull'istante in cui viene chiamata la funzione, il che non è puro semplicemente perché non puoi mai chiamare la funzione con gli stessi parametri due volte.

Se si desidera avere una versione pura di dollarToEurociò che può ancora ottenere il valore più aggiornato, è comunque possibile associare l'autorità storica, ma lasciare il parametro date non associato e chiedere la data al chiamante come argomento, finendo con qualcosa del genere:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());

@Snowman Prego! Ho aggiornato un po 'la risposta per aggiungere altri esempi di codice.
TheHansinator

8

Mi piacerebbe allontanarmi un po 'dai dettagli specifici di JS e dall'astrazione delle definizioni formali e parlare di quali condizioni devono essere mantenute per consentire specifiche ottimizzazioni. Di solito è la cosa principale a cui teniamo quando scriviamo il codice (anche se aiuta a dimostrare la correttezza). La programmazione funzionale non è né una guida alle ultime mode né un voto monastico di abnegazione. È uno strumento per risolvere i problemi.

Quando hai un codice come questo:

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Se exchangeRatenon è mai possibile modificarlo tra le due chiamate dollarToEuro(100), è possibile memorizzare il risultato della prima chiamata dollarToEuro(100)e ottimizzare la seconda chiamata. Il risultato sarà lo stesso, quindi possiamo solo ricordare il valore di prima.

Il exchangeRatepotrebbe essere impostata una volta, prima di chiamare qualsiasi funzione che guarda in su, e mai modificato. Meno restrittivamente, potresti avere un codice che cerca una exchangeRatevolta una particolare funzione o blocco di codice e utilizza lo stesso tasso di cambio in modo coerente all'interno di tale ambito. Oppure, se solo questo thread può modificare il database, si avrebbe il diritto di presumere che, se non si è aggiornato il tasso di cambio, nessun altro lo ha modificato su di te.

Se fetchFromDatabase()è essa stessa una funzione pura che valuta una costante ed exchangeRateè immutabile, potremmo piegare questa costante fino in fondo attraverso il calcolo. Un compilatore che sa che questo è il caso potrebbe fare la stessa deduzione che hai fatto nel commento, che dollarToEuro(100)valuta 90.0 e sostituire l'intera espressione con la costante 90.0.

Tuttavia, se fetchFromDatabase()non esegue l'I / O, che è considerato un effetto collaterale, il suo nome viola il principio del minimo stupore.


8

Questa funzione non è pura, si basa su una variabile esterna, che cambierà quasi sicuramente.

La funzione quindi non riesce al primo punto che hai fatto, non restituisce lo stesso valore quando per gli stessi argomenti.

Per rendere questa funzione "pura", passare exchangeRatecome argomento.

Ciò soddisferebbe quindi entrambe le condizioni.

  1. Restituirà sempre lo stesso valore quando si passa allo stesso valore e tasso di cambio.
  2. Inoltre non avrebbe effetti collaterali.

Codice di esempio:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())

1
"che quasi sicuramente cambierà" --- non lo è, lo è const.
zerkms

7

Per espandere i punti che altri hanno fatto sulla trasparenza referenziale: possiamo definire la purezza semplicemente come trasparenza referenziale delle chiamate di funzione (cioè ogni chiamata alla funzione può essere sostituita dal valore di ritorno senza cambiare la semantica del programma).

Le due proprietà fornite sono entrambe conseguenze della trasparenza referenziale. Ad esempio, la seguente funzione f1è impura, poiché non fornisce sempre lo stesso risultato (la proprietà che hai numerato 1):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

Perché è importante ottenere lo stesso risultato ogni volta? Perché ottenere risultati diversi è un modo per una chiamata di funzione di avere una semantica diversa da un valore, e quindi interrompere la trasparenza referenziale.

Diciamo che scriviamo il codice f1("hello", "world"), lo eseguiamo e otteniamo il valore restituito "hello". Se facciamo una ricerca / sostituzione di ogni chiamata f1("hello", "world")e la sostituiamo con "hello"avremo cambiato la semantica del programma (tutte le chiamate saranno ora sostituite da "hello", ma in origine circa la metà di esse avrebbe valutato "world"). Quindi le chiamate a f1non sono referenzialmente trasparenti, quindi f1è impuro.

Un altro modo in cui una chiamata di funzione può avere una semantica diversa da un valore è eseguendo istruzioni. Per esempio:

function f2(x) {
  console.log("foo");
  return x;
}

Il valore restituito di f2("bar")sarà sempre "bar", ma la semantica del valore "bar"è diversa dalla chiamata f2("bar")poiché quest'ultima accederà anche alla console. Sostituire l'uno con l'altro cambierebbe la semantica del programma, quindi non è referenzialmente trasparente, e quindi f2è impuro.

Se la tua dollarToEurofunzione è referenzialmente trasparente (e quindi pura) dipende da due cose:

  • La "portata" di ciò che consideriamo referenzialmente trasparente
  • Se exchangeRatecambieranno mai in questo "ambito"

Non esiste un ambito "migliore" da utilizzare; normalmente penseremmo a una singola esecuzione del programma o alla durata del progetto. Per analogia, immagina che i valori di ritorno di ogni funzione vengano memorizzati nella cache (come la tabella dei memo nell'esempio fornito da @ aadit-m-shah): quando avremmo bisogno di svuotare la cache, per garantire che i valori non aggiornati non interferiscano con i nostri semantica?

Se lo exchangeRatestessero usando var, potrebbe cambiare tra una chiamata e l'altra dollarToEuro; avremmo bisogno di cancellare tutti i risultati memorizzati nella cache tra ogni chiamata, quindi non ci sarebbe trasparenza referenziale di cui parlare.

Usando conststiamo espandendo l '"ambito" a un'esecuzione del programma: sarebbe sicuro memorizzare nella cache i valori di ritorno dollarToEurofino al termine del programma. Potremmo immaginare di usare una macro (in una lingua come Lisp) per sostituire le chiamate di funzione con i loro valori di ritorno. Questa quantità di purezza è comune per cose come valori di configurazione, opzioni della riga di comando o ID univoci. Se ci limitiamo a pensare a una corsa del programma, otteniamo la maggior parte dei vantaggi della purezza, ma dobbiamo stare attenti tra le varie corse (ad esempio, salvare i dati in un file, quindi caricarli in un'altra corsa). Non definirei tali funzioni "pure" in senso astratto (ad esempio se stavo scrivendo una definizione di dizionario), ma non avrei problemi a trattarle come pure nel contesto .

Se consideriamo la durata del progetto come il nostro 'ambito', allora siamo i "più referenzialmente trasparenti" e quindi il "più puro", anche in senso astratto. Non avremmo mai bisogno di cancellare la nostra ipotetica cache. Potremmo persino fare questo "caching" riscrivendo direttamente il codice sorgente sul disco, per sostituire le chiamate con i loro valori di ritorno. Funzionerebbe anche su più progetti, ad esempio potremmo immaginare un database online di funzioni e i loro valori di ritorno, dove chiunque può cercare una chiamata di funzione e (se si trova nel DB) utilizzare il valore di ritorno fornito da qualcuno dall'altra parte del mondo che ha usato una funzione identica anni fa su un progetto diverso.


4

Come scritto, è una funzione pura. Non produce effetti collaterali. La funzione ha un parametro formale, ma ha due input e produrrà sempre lo stesso valore per due input qualsiasi.


2

Possiamo chiamare tali funzioni funzioni pure. Se la risposta è NO, come possiamo quindi riformattarla in una sola?

Come hai giustamente notato, "potrebbe darmi un risultato diverso domani" . In tal caso, la risposta sarebbe un clamoroso "no" . Ciò è particolarmente vero se il comportamento previsto dollarToEuroè stato interpretato correttamente come:

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

Tuttavia, esiste un'interpretazione diversa, dove sarebbe considerata pura:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro direttamente sopra è puro.


Dal punto di vista dell'ingegneria del software, è essenziale dichiarare la dipendenza dollarToEurodalla funzione fetchFromDatabase. Pertanto, riformattare la definizione dollarToEurocome segue:

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

Con questo risultato, data la premessa che fetchFromDatabasefunziona in modo soddisfacente, allora possiamo concludere che la proiezione di fetchFromDatabaseon dollarToEurodeve essere soddisfacente. Oppure l'affermazione " fetchFromDatabaseè puro" implica che dollarToEuroè puro (poiché fetchFromDatabaseè una base per dollarToEuroil fattore scalare di x.

Dal post originale, posso capire che fetchFromDatabaseè un tempo di funzione. Miglioriamo lo sforzo di refactoring per rendere trasparente questa comprensione, quindi chiaramente qualificabile fetchFromDatabasecome pura funzione:

fetchFromDatabase = (timestamp) => {/ * qui va l'implementazione * /};

Alla fine, refactoring la funzionalità come segue:

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

Di conseguenza, dollarToEuropuò essere testato in unità semplicemente dimostrando che chiama correttamente fetchFromDatabase(o il suo derivato exchangeRate).


1
Questo è stato molto illuminante. +1. Grazie.
Pupazzo di neve

Mentre trovo la tua risposta più istruttiva, e forse il miglior refactoring per il particolare caso d'uso di dollarToEuro; Nel PO ho menzionato che potrebbero esserci altri casi d'uso. Ho scelto dollarToEuro perché evoca immediatamente ciò che sto cercando di fare, ma potrebbe esserci qualcosa di meno sottile che dipende da una variabile libera che può cambiare, ma non necessariamente in funzione del tempo. Con questo in mente, trovo che il refattore votato per primo sia il più accessibile e quello che può aiutare gli altri con casi d'uso simili. Grazie per il tuo aiuto a prescindere.
Pupazzo di neve,

-1

Sono un bilingue Haskell / JS e Haskell è una delle lingue che fa molto per la purezza delle funzioni, quindi ho pensato di darti la prospettiva di come la vede Haskell.

Come altri hanno già detto, in Haskell la lettura di una variabile mutabile è generalmente considerata impura. C'è una differenza tra variabili e definizioni in quanto le variabili possono cambiare in seguito, le definizioni sono le stesse per sempre. Quindi, se l' avessi dichiarato constallora (supponendo che sia solo una numbere non abbia una struttura interna mutevole), leggere da ciò sarebbe usare una definizione, che è pura. Ma volevi modellare i tassi di cambio che cambiano nel tempo, e questo richiede una sorta di mutabilità e quindi ti metti nell'impurità.

Per descrivere quel tipo di cose impure (possiamo chiamarle "effetti" e il loro uso "efficace" in contrapposizione a "puro") in Haskell, facciamo ciò che potreste chiamare metaprogrammazione . Oggi la metaprogrammazione di solito si riferisce a macro che non è ciò che intendo, ma piuttosto l'idea di scrivere un programma per scrivere un altro programma in generale.

In questo caso, in Haskell, scriviamo un calcolo puro che calcola un programma efficace che farà quindi ciò che vogliamo. Quindi l'intero punto di un file sorgente di Haskell (almeno uno che descrive un programma, non una libreria) è descrivere un calcolo puro per un programma efficace che produce vuoto, chiamato main. Quindi il compito del compilatore Haskell è prendere questo file sorgente, eseguire quel calcolo puro e mettere quel programma efficace come eseguibile binario da qualche parte sul tuo disco rigido per essere eseguito in seguito a tuo piacimento. C'è un divario, in altre parole, tra il momento in cui viene eseguito il calcolo puro (mentre il compilatore rende eseguibile) e il momento in cui viene eseguito il programma efficace (ogni volta che si esegue l'eseguibile).

Quindi per noi, i programmi efficaci sono in realtà una struttura di dati e non fanno intrinsecamente nulla solo venendo menzionati (non hanno * effetti collaterali * oltre al loro valore di ritorno; il loro valore di ritorno contiene i loro effetti). Per un esempio molto leggero di una classe TypeScript che descrive programmi immutabili e alcune cose che puoi fare con loro,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

La chiave è che se non Program<x>si verificano effetti collaterali, si tratta di entità totalmente funzionalmente pure. La mappatura di una funzione su un programma non ha effetti collaterali a meno che la funzione non sia pura; il sequenziamento di due programmi non ha effetti collaterali; eccetera.

Quindi, ad esempio, come applicare questo nel tuo caso, potresti scrivere alcune funzioni pure che restituiscono programmi per ottenere utenti in base all'ID e per modificare un database e recuperare dati JSON, come

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

e quindi potresti descrivere un lavoro cron per arricciare un URL e cercare un dipendente e informare il proprio supervisore in modo puramente funzionale come

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

Il punto è che ogni singola funzione qui è una funzione completamente pura; nulla è realmente accaduto fino a quando non l'ho action.run()messo in moto. Inoltre posso scrivere funzioni come

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

e se JS avesse promesso la cancellazione, potremmo far correre due programmi l'uno con l'altro e prendere il primo risultato e cancellare il secondo. (Voglio dire che possiamo ancora, ma diventa meno chiaro cosa fare.)

Allo stesso modo nel tuo caso possiamo descrivere i tassi di cambio con

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

e exchangeRatepotrebbe essere un programma che guarda un valore mutabile,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

ma anche così, questa funzione dollarsToEurosè ora una funzione pura da un numero a un programma che produce un numero, e puoi ragionare su di esso in quel modo equo deterministico che puoi ragionare su qualsiasi programma che non ha effetti collaterali.

Il costo, ovviamente, è che alla fine devi chiamarlo da .run() qualche parte , e sarà impuro. Ma l'intera struttura del tuo calcolo può essere descritta da un puro calcolo, e puoi spingere l'impurità ai margini del tuo codice.


Sono curioso di sapere perché questo continua a subire un downgrade, ma intendo ancora sostenerlo (è, infatti, il modo in cui manipoli i programmi in Haskell dove le cose sono pure per impostazione predefinita) e volentieri riempie i voti negativi. Tuttavia, se i downvoter volessero lasciare commenti che spiegano cosa non gli piace, posso provare a migliorarlo.
CR Drost,

Sì, mi chiedevo perché ci sono così tanti voti negativi ma non un singolo commento, oltre ovviamente all'autore.
Buda Örs,
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.