Secara teknis, setiap program yang Anda jalankan di komputer tidak murni karena pada akhirnya mengkompilasi ke instruksi seperti "pindahkan nilai ini ke eax
" dan "tambahkan nilai ini ke konten eax
", yang tidak murni. Itu tidak terlalu membantu.
Sebaliknya, kami berpikir tentang kemurnian menggunakan kotak hitam . Jika beberapa kode selalu menghasilkan output yang sama ketika diberi input yang sama maka itu dianggap murni. Dengan definisi ini, fungsi berikut ini juga murni meskipun secara internal ia menggunakan tabel memo tidak murni.
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));
Kami tidak peduli dengan internal karena kami menggunakan metodologi kotak hitam untuk memeriksa kemurnian. Demikian pula, kami tidak peduli bahwa semua kode pada akhirnya dikonversi menjadi instruksi mesin yang tidak murni karena kami berpikir tentang kemurnian menggunakan metodologi kotak hitam. Internal tidak penting.
Sekarang, pertimbangkan fungsi berikut.
const greet = name => {
console.log("Hello %s!", name);
};
greet("World");
greet("Snowman");
Adalah greet
fungsinya murni atau tidak murni? Dengan metodologi kotak hitam kami, jika kami memberikan input yang sama (misalnya World
) maka selalu mencetak output yang sama ke layar (yaituHello World!
). Dalam pengertian itu, bukankah itu murni? Tidak, tidak. Alasan itu tidak murni adalah karena kami menganggap mencetak sesuatu ke layar sebagai efek samping. Jika kotak hitam kami menghasilkan efek samping maka itu tidak murni.
Apa itu efek samping? Di sinilah konsep transparansi referensial berguna. Jika suatu fungsi secara transparan transparan maka kita selalu dapat mengganti aplikasi dari fungsi itu dengan hasilnya. Perhatikan bahwa ini tidak sama dengan fungsi inlining .
Dalam fungsi inlining, kami mengganti aplikasi fungsi dengan tubuh fungsi tanpa mengubah semantik program. Namun, fungsi transparan referensial selalu dapat diganti dengan nilai kembali tanpa mengubah semantik program. Perhatikan contoh berikut.
console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");
Di sini, kami menggariskan definisi greet
dan tidak mengubah semantik program.
Sekarang, pertimbangkan program berikut.
Di sini, kami mengganti aplikasi greet
fungsi dengan nilai kembali dan itu memang mengubah semantik program. Kami tidak lagi mencetak salam ke layar. Itulah alasan mengapa pencetakan dianggap sebagai efek samping, dan itulah sebabnyagreet
fungsinya tidak murni. Ini tidak transparan secara referensial.
Sekarang, mari kita pertimbangkan contoh lain. Pertimbangkan program berikut.
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();
Jelas, main
fungsinya tidak murni. Namun, apakah timeDiff
fungsinya murni atau tidak murni? Meskipun itu tergantungserverTime
yang berasal dari panggilan jaringan tidak murni, itu masih transparan secara referensial karena ia mengembalikan output yang sama untuk input yang sama dan karena itu tidak memiliki efek samping.
Zerkms mungkin akan tidak setuju dengan saya dalam hal ini. Dalam jawabannya , ia mengatakan bahwa dollarToEuro
fungsi dalam contoh berikut tidak murni karena "itu tergantung pada IO secara transitif."
const exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x, exchangeRate) => {
return x * exchangeRate;
};
Saya harus tidak setuju dengannya karena fakta bahwa exchangeRate
databasenya tidak relevan. Ini detail internal dan metodologi kotak hitam kami untuk menentukan kemurnian fungsi tidak peduli dengan detail internal.
Dalam bahasa yang murni fungsional seperti Haskell, kami memiliki jalan keluar untuk mengeksekusi efek IO yang arbitrer. Ini disebut unsafePerformIO
, dan seperti namanya jika Anda tidak menggunakannya dengan benar maka itu tidak aman karena dapat merusak transparansi referensial. Namun, jika Anda tahu apa yang Anda lakukan maka itu sangat aman untuk digunakan.
Ini umumnya digunakan untuk memuat data dari file konfigurasi di dekat awal program. Memuat data dari file konfigurasi adalah operasi IO yang tidak murni. Namun, kami tidak ingin terbebani dengan mengirimkan data sebagai input ke setiap fungsi. Makanya, jika kita gunakanunsafePerformIO
maka kita dapat memuat data di tingkat atas dan semua fungsi murni kita dapat bergantung pada data konfigurasi global yang tidak dapat diubah.
Perhatikan bahwa hanya karena suatu fungsi bergantung pada beberapa data yang dimuat dari file konfigurasi, database, atau panggilan jaringan, tidak berarti bahwa fungsi tersebut tidak murni.
Namun, mari kita perhatikan contoh asli Anda yang memiliki semantik berbeda.
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
Di sini, saya berasumsi bahwa karena exchangeRate
tidak didefinisikan sebagai const
, itu akan dimodifikasi ketika program sedang berjalan. Jika itu yang terjadi maka dollarToEuro
pasti fungsi yang tidak murni karena ketikaexchangeRate
dimodifikasi, itu akan merusak transparansi referensial.
Namun, jika exchangeRate
variabel tidak dimodifikasi dan tidak akan pernah dimodifikasi di masa mendatang (yaitu jika itu adalah nilai konstan), maka meskipun itu didefinisikan sebagai let
, itu tidak akan merusak transparansi referensial. Dalam hal ini, dollarToEuro
memang fungsi murni.
Perhatikan bahwa nilai exchangeRate
dapat berubah setiap kali Anda menjalankan program lagi dan itu tidak akan merusak transparansi referensial. Itu hanya merusak transparansi referensial jika itu berubah saat program sedang berjalan.
Misalnya, jika Anda menjalankan timeDiff
contoh saya beberapa kali maka Anda akan mendapatkan nilai yang berbeda untuk serverTime
dan karenanya hasil yang berbeda. Namun, karena nilai serverTime
tidak pernah berubah saat program sedang berjalan, timeDiff
fungsinya murni.
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);