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 greet
funzione è 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 greet
e non ha cambiato la semantica del programma.
Ora, considera il seguente programma.
Qui, abbiamo sostituito le applicazioni della greet
funzione 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 greet
funzione è 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 main
funzione è impura. Tuttavia, la timeDiff
funzione è pura o impura? Anche se dipende da serverTime
quale 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 dollarToEuro
funzione 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 exchangeRate
provenga 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 unsafePerformIO
e, 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é exchangeRate
non è definito come const
, verrà modificato mentre il programma è in esecuzione. In tal caso, allora dollarToEuro
è sicuramente una funzione impura perché quando exchangeRate
viene modificata, si romperà la trasparenza referenziale.
Tuttavia, se la exchangeRate
variabile 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 exchangeRate
può 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 timeDiff
esempio più volte, otterrai valori diversi serverTime
e quindi risultati diversi. Tuttavia, poiché il valore di serverTime
non cambia mai mentre il programma è in esecuzione, la timeDiff
funzione è pura.
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);