javascript - Questa è una funzione pura?




function functional-programming (7)

La maggior parte delle sources 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:

(EDIT - Usando const nella prima riga. Usato let precedentemente 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'ingresso 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?


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 di dollarToEuro è stato correttamente interpretato 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 di dollarToEuro dalla funzione fetchFromDatabase . Pertanto, dollarToEuro la definizione di dollarToEuro come segue:

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

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

Dal post originale, posso capire che fetchFromDatabase è un tempo di funzione. fetchFromDatabase lo sforzo di refactoring per rendere trasparente quella comprensione, quindi chiaramente qualificare fetchFromDatabase come pura funzione:

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

Alla fine, rifarrei la funzione 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, dollarToEuro può essere testato in unità semplicemente dimostrando che chiama correttamente fetchFromDatabase (o la sua derivata exchangeRate ).


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, dollarToEuro sembra 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 dollarToEuro ma dollarToEuroAtInstantOfProgramStart .

La chiave qui è che ci sono diversi parametri necessari per calcolare una conversione di valuta e che una versione veramente pura del dollarToEuro generale dollarToEuro li dollarToEuro 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 dollarToEuro 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 dollarToEuro che ottiene sempre il valore più aggiornato essenzialmente prende implicitamente il parametro date, impostandolo dollarToEuro cui viene chiamata la funzione, il che non è puro semplicemente perché non puoi mai chiamare la funzione con gli stessi parametri due volte .

Se vuoi avere una versione pura di dollarToEuro che possa ancora ottenere il valore più aggiornato, puoi comunque associare l'autorità storica, ma lascia il parametro date non associato e chiedere la data al chiamante come argomento, terminando con qualcosa del genere:

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

dollarToEuro(100, Date.now());

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.


Il valore di ritorno di dollarToEuro dipende da una variabile esterna che non è un argomento, quindi è impura.

Nella risposta è NO, come possiamo quindi riformattarlo in uno?

Un'opzione sarebbe passare in exchangeRate . In questo modo, ogni volta che gli argomenti sono (something, somethingElse) , l'output è garantito per essere esattamente something * somethingElse :

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

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

Nota che per la programmazione funzionale, dovresti assolutamente evitare anche let - usa sempre const , in modo da evitare la riassegnazione.


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 di ritorno "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 essi avrebbe valutato il "world" ). Quindi le chiamate a f1 non 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 di ritorno di f2("bar") sarà sempre "bar" , ma la semantica del valore "bar" è diversa dalla chiamata f2("bar") poiché anche quest'ultimo accederà alla console. Sostituire l'uno con l'altro cambierebbe la semantica del programma, quindi non è referenzialmente trasparente, e quindi f2 è impuro.

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

  • La "portata" di ciò che consideriamo referenzialmente trasparente
  • Se exchangeRate cambierà mai all'interno di tale "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 exchangeRate utilizzava var , potrebbe cambiare da una chiamata a dollarToEuro ; avremmo bisogno di cancellare tutti i risultati memorizzati nella cache tra ogni chiamata, quindi non ci sarebbe trasparenza referenziale di cui parlare.

Usando const stiamo espandendo l '"ambito" in un'esecuzione del programma: sarebbe sicuro memorizzare nella cache i valori di ritorno di dollarToEuro fino 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 es. Salvataggio dei dati in un file, quindi caricamento 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 i "più puri", 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, in cui 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.


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 exchangeRate come 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())

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 Object.prototype.valueOf tipi di base di patch o creare tipi personalizzati usando funzionalità come Object.prototype.valueOf è impossibile dire se una funzione è pura solo guardandola, poiché dipende dal chiamante se vogliono 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 sources

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, vedi l'altra risposta per i dettagli: https://.com/a/58749249/251311

Altre risorse pertinenti:





functional-programming