Apakah ini fungsi murni?

117

Sebagian besar sumber mendefinisikan fungsi murni sebagai memiliki dua properti berikut:

  1. Nilai kembalinya sama untuk argumen yang sama.
  2. Evaluasi tidak memiliki efek samping.

Ini adalah kondisi pertama yang mengkhawatirkan saya. Dalam kebanyakan kasus, mudah untuk menilai. Pertimbangkan fungsi-fungsi JavaScript berikut (seperti yang ditunjukkan dalam artikel ini )

Murni:

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

add(2, 4); // 6

Najis:

let x = 2;

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

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

Sangat mudah untuk melihat bahwa fungsi ke-2 akan memberikan output yang berbeda untuk panggilan berikutnya, sehingga melanggar kondisi pertama. Dan karenanya, itu tidak murni.

Bagian ini saya dapatkan.


Sekarang, untuk pertanyaan saya, pertimbangkan fungsi ini yang mengubah jumlah tertentu dalam dolar menjadi euro:

(EDIT - Menggunakan constdi baris pertama. Digunakan letsebelumnya secara tidak sengaja.)

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

Anggaplah kita mengambil nilai tukar dari db dan itu berubah setiap hari.

Sekarang, tidak peduli berapa kali saya memanggil fungsi ini hari ini , itu akan memberi saya output yang sama untuk input 100. Namun, itu mungkin memberi saya hasil yang berbeda besok. Saya tidak yakin apakah ini melanggar kondisi pertama atau tidak.

TKI, fungsi itu sendiri tidak mengandung logika untuk mengubah input, tetapi bergantung pada konstanta eksternal yang mungkin berubah di masa depan. Dalam hal ini, sangat pasti akan berubah setiap hari. Dalam kasus lain, itu mungkin terjadi; mungkin tidak.

Bisakah kita memanggil fungsi seperti fungsi murni. Jika jawabannya TIDAK, bagaimana kita bisa menolaknya menjadi satu?

Manusia salju
sumber
6
Kemurnian bahasa yang begitu dinamis seperti JS adalah topik yang sangat rumit: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
Purity berarti Anda dapat mengganti panggilan fungsi dengan nilai hasilnya di tingkat kode tanpa mengubah perilaku program Anda.
bob
1
Untuk sedikit lebih jauh tentang apa yang merupakan efek samping, dan dengan terminologi yang lebih teoretis, lihat cs.stackexchange.com/questions/116377/...
Gilles 'SO-stop being evil'
3
Hari ini, fungsinya adalah (x) => {return x * 0.9;}. Besok, Anda akan memiliki fungsi berbeda yang juga murni, mungkin (x) => {return x * 0.89;}. Perhatikan bahwa setiap kali Anda menjalankannya (x) => {return x * exchangeRate;}menciptakan fungsi baru , dan fungsi itu murni karena exchangeRatetidak dapat berubah.
user253751
2
Ini adalah fungsi yang tidak murni, Jika Anda ingin menjadikannya murni, Anda dapat menggunakan const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; untuk fungsi murni, Its return value is the same for the same arguments.harus selalu memegang, 1 detik, 1 dekade .. nanti tidak peduli apa
Vikash Tiwari

Jawaban:

133

Nilai dollarToEuropengembalian tergantung pada variabel luar yang bukan argumen; oleh karena itu, fungsinya tidak murni.

Dalam jawabannya TIDAK, bagaimana kita bisa memperbaiki fungsi menjadi murni?

Salah satu opsi adalah lewat exchangeRate. Dengan cara ini, setiap kali ada argumen (something, somethingElse), output dijamin menjadi something * somethingElse:

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

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

Perhatikan bahwa untuk pemrograman fungsional, Anda harus menghindari let- selalu gunakan constuntuk menghindari penugasan kembali.

Performa Tertentu
sumber
6
Tidak memiliki variabel bebas tidak persyaratan untuk fungsi menjadi murni: const add = x => y => x + y; const one = add(42);Berikut baik adddan oneadalah fungsi murni.
zerkms
7
const foo = 42; const add42 = x => x + foo;<- ini adalah fungsi murni lain, yang lagi-lagi menggunakan variabel bebas.
zerkms
8
@zerkms - Saya akan sangat tertarik untuk melihat jawaban Anda untuk pertanyaan ini (bahkan jika itu hanya rewords dari SomePerformance untuk menggunakan terminologi yang berbeda). Saya tidak berpikir itu akan menduplikasi, dan itu akan mencerahkan, terutama ketika dikutip (idealnya dengan sumber yang lebih baik daripada artikel Wikipedia di atas, tetapi jika hanya itu yang kita dapatkan, masih menang). (Akan mudah untuk membaca komentar ini dalam semacam cahaya negatif. Percayalah bahwa saya tulus, saya pikir jawaban seperti itu akan bagus dan ingin membacanya.)
TJ Crowder
17
Saya pikir Anda dan @zerkms salah. Anda tampaknya berpikir bahwa dollarToEurofungsi dalam contoh dalam jawaban Anda tidak murni karena tergantung pada variabel bebas exchangeRate. Itu tidak masuk akal. Seperti yang ditunjukkan zerkms, kemurnian suatu fungsi tidak ada hubungannya dengan apakah ia memiliki variabel bebas atau tidak. Namun, zerkms juga salah karena dia percaya bahwa dollarToEurofungsinya tidak murni karena itu tergantung dari exchangeRatemana yang berasal dari database. Dia mengatakan itu tidak murni karena "itu tergantung pada IO secara transitif."
Aadit M Shah
9
(lanjutan) Sekali lagi, itu tidak masuk akal karena ini menunjukkan bahwa dollarToEuroitu tidak murni karena exchangeRatemerupakan variabel bebas. Ini menunjukkan bahwa jika exchangeRatebukan variabel bebas, yaitu jika itu adalah argumen, maka dollarToEuroakan menjadi murni. Oleh karena itu, ini menunjukkan bahwa dollarToEuro(100)itu tidak murni tetapi dollarToEuro(100, exchangeRate)murni. Itu jelas tidak masuk akal karena dalam kedua kasus Anda tergantung pada exchangeRateyang berasal dari database. Satu-satunya perbedaan adalah apakah exchangeRatevariabel bebas di dalam dollarToEurofungsi.
Aadit M Shah
76

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 greetdan tidak mengubah semantik program.

Sekarang, pertimbangkan program berikut.

undefined;
undefined;

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, mainfungsinya tidak murni. Namun, apakah timeDifffungsinya 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 dollarToEurofungsi 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 exchangeRatedatabasenya 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 exchangeRatetidak didefinisikan sebagai const, itu akan dimodifikasi ketika program sedang berjalan. Jika itu yang terjadi maka dollarToEuropasti fungsi yang tidak murni karena ketikaexchangeRate dimodifikasi, itu akan merusak transparansi referensial.

Namun, jika exchangeRatevariabel 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, dollarToEuromemang fungsi murni.

Perhatikan bahwa nilai exchangeRatedapat 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 timeDiffcontoh saya beberapa kali maka Anda akan mendapatkan nilai yang berbeda untuk serverTimedan karenanya hasil yang berbeda. Namun, karena nilai serverTimetidak pernah berubah saat program sedang berjalan, timeDifffungsinya murni.

Aadit M Shah
sumber
3
Ini sangat informatif. Terima kasih. Dan saya memang bermaksud menggunakan constdalam contoh saya.
Snowman
3
Jika Anda memang bermaksud menggunakan constmaka dollarToEurofungsinya memang murni. Satu-satunya cara nilai exchangeRateakan berubah adalah jika Anda menjalankan program lagi. Dalam hal ini, proses lama dan proses baru berbeda. Oleh karena itu, itu tidak melanggar transparansi referensial. Ini seperti memanggil fungsi dua kali dengan argumen yang berbeda. Argumen mungkin berbeda tetapi dalam fungsi nilai argumen tetap konstan.
Aadit M Shah
3
Ini kedengarannya seperti teori kecil tentang relativitas: konstanta hanya relatif konstan, tidak mutlak, yaitu relatif terhadap proses yang berjalan. Jelas satu-satunya jawaban yang benar di sini. +1.
bob
5
Saya tidak setuju dengan "tidak murni karena pada akhirnya mengkompilasi ke instruksi seperti" pindahkan nilai ini ke eax "dan" tambahkan nilai ini ke isi eax " . Jika eaxdihapus - melalui suatu beban atau yang jelas - kode tetap deterministik terlepas dari apa lagi yang terjadi dan karenanya murni. Kalau tidak, jawaban yang sangat komprehensif.
3Dave
3
@Bergi: Sebenarnya, dalam bahasa murni dengan nilai-nilai abadi, identitas tidak relevan. Apakah dua referensi yang mengevaluasi ke nilai yang sama adalah dua referensi ke objek yang sama atau ke objek yang berbeda hanya dapat diamati dengan bermutasi objek melalui salah satu referensi dan mengamati apakah nilai juga berubah ketika diambil melalui referensi lain. Tanpa mutasi, identitas menjadi tidak relevan. (Seperti yang akan dikatakan Rich Hickey: Identitas adalah serangkaian Negara dari Waktu.)
Jörg W Mittag
23

Sebuah jawaban dari saya-purist (di mana "saya" secara harfiah adalah saya, karena saya pikir pertanyaan ini tidak memiliki jawaban formal "benar" tunggal ):

Dalam bahasa dinamis seperti JS dengan begitu banyak kemungkinan untuk menggunakan tipe dasar tambalan, atau membuat jenis kustom menggunakan fitur seperti Object.prototype.valueOftidak mungkin untuk mengetahui apakah suatu fungsi murni hanya dengan melihatnya, karena tergantung pada pemanggil apakah mereka ingin. untuk menghasilkan efek samping.

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

Jawaban saya-pragmatis:

Dari definisi dari wikipedia

Dalam pemrograman komputer, fungsi murni adalah fungsi yang memiliki properti berikut:

  1. Nilai kembalinya sama untuk argumen yang sama (tidak ada variasi dengan variabel statis lokal, variabel non-lokal, argumen referensi yang dapat diubah atau input stream dari perangkat I / O).
  2. Evaluasi tidak memiliki efek samping (tidak ada mutasi variabel statis lokal, variabel non-lokal, argumen referensi yang dapat diubah atau aliran I / O).

Dengan kata lain, itu hanya masalah bagaimana suatu fungsi berperilaku, bukan bagaimana itu diterapkan. Dan selama fungsi tertentu menyimpan 2 properti ini - murni terlepas dari bagaimana tepatnya itu diterapkan.

Sekarang ke fungsi Anda:

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

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

Itu tidak murni karena tidak memenuhi syarat 2: itu tergantung pada IO secara transitif.

Saya setuju pernyataan di atas salah, lihat jawaban lain untuk detail: https://stackoverflow.com/a/58749249/251311

Sumber daya relevan lainnya:

zerkms
sumber
4
@TJCrowder mesebagai zerkms yang memberikan jawaban.
zerkms
2
Ya, dengan Javascript itu semua tentang kepercayaan, bukan jaminan
bob
4
@ Bob ... atau itu adalah panggilan pemblokiran.
zerkms
1
@zerkms - Terima kasih. Jadi saya 100% yakin, perbedaan utama antara Anda add42dan saya addXadalah murni bahwa saya xdapat diubah, dan Anda fttidak dapat diubah (dan karenanya, add42nilai pengembalian tidak bervariasi berdasarkan pada ft)?
TJ Crowder
5
Saya tidak setuju bahwa dollarToEurofungsi dalam contoh Anda tidak murni. Saya menjelaskan mengapa saya tidak setuju dengan jawaban saya. stackoverflow.com/a/58749249/783743
Aadit M Shah
14

Seperti jawaban lain katakan, cara Anda menerapkan dollarToEuro,

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

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

memang murni, karena nilai tukar tidak diperbarui saat program sedang berjalan. Namun secara konseptual, dollarToEurotampaknya itu harus menjadi fungsi yang tidak murni, karena menggunakan nilai tukar apa pun yang paling mutakhir. Cara paling sederhana untuk menjelaskan perbedaan ini adalah bahwa Anda belum menerapkan dollarToEurotapi dollarToEuroAtInstantOfProgramStart.

Kuncinya di sini adalah bahwa ada beberapa parameter yang diperlukan untuk menghitung konversi mata uang, dan bahwa versi jenderal yang benar-benar murni dollarToEuroakan memasok semuanya. Parameter paling langsung adalah jumlah USD untuk dikonversi, dan nilai tukar. Namun, karena Anda ingin mendapatkan nilai tukar dari informasi yang dipublikasikan, Anda sekarang memiliki tiga parameter untuk diberikan:

  • Jumlah uang yang ditukar
  • Otoritas historis untuk berkonsultasi untuk nilai tukar
  • Tanggal terjadinya transaksi (untuk mengindeks otoritas historis)

Otoritas historis di sini adalah basis data Anda, dan dengan asumsi bahwa basis data tidak dikompromikan, akan selalu mengembalikan hasil yang sama untuk nilai tukar pada hari tertentu. Karenanya, dengan kombinasi ketiga parameter ini, Anda dapat menulis versi umum dollarToEuroyang sepenuhnya murni dan mandiri , yang mungkin terlihat seperti ini:

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

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

Implementasi Anda menangkap nilai konstan untuk otoritas historis dan tanggal transaksi saat fungsi dibuat - otoritas historis adalah basis data Anda, dan tanggal yang diambil adalah tanggal saat Anda memulai program - yang tersisa hanyalah jumlah dolar , yang disediakan pemanggil. Versi tidak murni daridollarToEuro yang selalu mendapatkan nilai paling terbaru pada dasarnya mengambil parameter tanggal secara implisit, mengaturnya ke instan fungsi dipanggil, yang tidak murni hanya karena Anda tidak pernah dapat memanggil fungsi dengan parameter yang sama dua kali.

Jika Anda ingin memiliki versi murni dollarToEuroyang masih bisa mendapatkan nilai terbaru, Anda masih dapat mengikat otoritas historis, tetapi membiarkan parameter tanggal tidak terikat dan meminta tanggal dari penelepon sebagai argumen, berakhir dengan sesuatu seperti ini:

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

dollarToEuro(100, Date.now());
TheHansinator
sumber
@Snowman Sama-sama! Saya memperbarui jawabannya sedikit untuk menambahkan lebih banyak contoh kode.
TheHansinator
8

Saya ingin sedikit mundur dari perincian spesifik JS dan abstraksi definisi formal, dan berbicara tentang kondisi mana yang perlu dipertahankan untuk memungkinkan optimalisasi spesifik. Itu biasanya hal utama yang kami pedulikan ketika menulis kode (meskipun itu juga membantu membuktikan kebenaran). Pemrograman fungsional bukan panduan untuk mode terbaru atau sumpah monastik penyangkalan diri. Ini adalah alat untuk menyelesaikan masalah.

Ketika Anda memiliki kode seperti ini:

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

Jika exchangeRatetidak pernah dapat dimodifikasi di antara dua panggilan dollarToEuro(100), dimungkinkan untuk mem-memo hasil panggilan pertama ke dollarToEuro(100)dan mengoptimalkan panggilan kedua. Hasilnya akan sama, jadi kita bisa mengingat nilainya dari sebelumnya.

The exchangeRatemungkin diatur sekali, sebelum memanggil fungsi apapun yang terlihat itu, dan tidak pernah dimodifikasi. Tidak terlalu membatasi, Anda mungkin memiliki kode yang mencari exchangeRatesekali fungsi atau blok kode tertentu, dan menggunakan nilai tukar yang sama secara konsisten dalam lingkup itu. Atau, jika hanya utas ini yang dapat memodifikasi basis data, Anda akan berhak berasumsi bahwa, jika Anda tidak memperbarui nilai tukar, tidak ada orang lain yang mengubahnya pada Anda.

Jika fetchFromDatabase()itu sendiri merupakan fungsi murni yang mengevaluasi ke sebuah konstanta, dan exchangeRatetidak dapat diubah, kita dapat melipat konstanta ini sepanjang jalan melalui perhitungan. Kompiler yang mengetahui hal ini sebagai kasusnya dapat membuat pengurangan yang sama yang Anda lakukan dalam komentar, yang dollarToEuro(100)mengevaluasi ke 90.0, dan mengganti seluruh ekspresi dengan konstan 90.0.

Namun, jika fetchFromDatabase()tidak melakukan I / O, yang dianggap sebagai efek samping, namanya melanggar Prinsip Least Astonishment.

Davislor
sumber
8

Fungsi ini tidak murni, itu bergantung pada variabel luar, yang hampir pasti akan berubah.

Karena itu fungsi gagal pada titik pertama yang Anda buat, tidak mengembalikan nilai yang sama ketika untuk argumen yang sama.

Untuk membuat fungsi ini "murni", berikan exchangeRateargumen.

Ini kemudian akan memenuhi kedua kondisi tersebut.

  1. Itu akan selalu mengembalikan nilai yang sama ketika melewati nilai dan nilai tukar yang sama.
  2. Itu juga tidak memiliki efek samping.

Kode contoh:

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

dollarToEuro(100, fetchFromDatabase())
Jessica
sumber
1
"Yang hampir pasti akan berubah" --- bukan, bukan const.
zerkms
7

Untuk memperluas poin yang telah dibuat orang lain tentang transparansi referensial: kita dapat mendefinisikan kemurnian sebagai sekadar transparansi referensial panggilan fungsi (yaitu setiap panggilan ke fungsi dapat diganti dengan nilai pengembalian tanpa mengubah semantik program).

Dua properti yang Anda berikan adalah konsekuensi transparansi referensial. Misalnya, fungsi berikut f1tidak murni, karena tidak memberikan hasil yang sama setiap kali (properti yang Anda beri nomor 1):

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

Mengapa penting untuk mendapatkan hasil yang sama setiap saat? Karena mendapatkan hasil yang berbeda adalah salah satu cara pemanggilan fungsi untuk memiliki semantik yang berbeda dari suatu nilai, dan karenanya memecah transparansi referensial.

Katakanlah kita menulis kode f1("hello", "world"), kita menjalankannya dan mendapatkan nilai balik "hello". Jika kami menemukan / mengganti setiap panggilan f1("hello", "world")dan menggantinya dengan "hello"kami, kami akan mengubah semantik program (semua panggilan sekarang akan diganti "hello", tetapi semula sekitar setengahnya akan dievaluasi "world"). Karenanya panggilan untuk f1tidak transparan secara referensial, karenanya f1tidak murni.

Cara lain bahwa panggilan fungsi dapat memiliki semantik berbeda dengan nilai adalah dengan mengeksekusi pernyataan. Sebagai contoh:

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

Nilai kembali f2("bar")akan selalu "bar", tetapi semantik nilai "bar"berbeda dari panggilan f2("bar")karena yang terakhir juga akan masuk ke konsol. Mengganti satu dengan yang lain akan mengubah semantik program, sehingga tidak transparan secara referensi, dan karenanya f2tidak murni.

Apakah dollarToEurofungsi Anda transparan secara referensi (dan karenanya murni) tergantung pada dua hal:

  • 'Cakupan' dari apa yang kami anggap transparan transparan
  • Apakah exchangeRateakan pernah berubah dalam 'ruang lingkup' itu

Tidak ada ruang lingkup "terbaik" untuk digunakan; biasanya kita akan berpikir tentang satu kali menjalankan program, atau masa hidup proyek. Sebagai analogi, bayangkan bahwa nilai fungsi setiap cache di-cache (seperti tabel memo dalam contoh yang diberikan oleh @ aadit-m-shah): kapan kita perlu menghapus cache, untuk memastikan bahwa nilai basi tidak akan mengganggu kita semantik?

Jika exchangeRatesedang menggunakan varmaka itu bisa berubah antara setiap panggilan ke dollarToEuro; kita perlu menghapus hasil cache di antara setiap panggilan, sehingga tidak akan ada transparansi referensial untuk dibicarakan.

Dengan menggunakan constkami memperluas 'lingkup' ke menjalankan program: itu akan aman untuk cache nilai kembali dollarToEurosampai program selesai. Kita bisa membayangkan menggunakan makro (dalam bahasa seperti Lisp) untuk mengganti panggilan fungsi dengan nilai kembali mereka. Jumlah kemurnian ini biasa untuk hal-hal seperti nilai konfigurasi, opsi baris perintah, atau ID unik. Jika kita membatasi diri kita untuk berpikir tentang satu kali menjalankan program maka kita mendapatkan sebagian besar manfaat dari kemurnian, tetapi kita harus berhati-hati di seluruh proses (misalnya menyimpan data ke file, kemudian memuatnya dalam menjalankan lain). Saya tidak akan menyebut fungsi seperti itu "murni" dalam pengertian abstrak (misalnya jika saya menulis definisi kamus), tetapi tidak memiliki masalah dengan memperlakukannya sebagai murni dalam konteks .

Jika kita memperlakukan masa proyek sebagai 'ruang lingkup' kita, maka kita adalah "yang paling transparan referensial" dan karenanya "yang paling murni", bahkan dalam arti abstrak. Kita tidak perlu menghapus cache hipotetis kita. Kami bahkan bisa melakukan "caching" ini dengan langsung menulis ulang kode sumber pada disk, untuk mengganti panggilan dengan nilai baliknya. Ini bahkan akan bekerja di seluruh proyek, misalnya kita bisa membayangkan database online fungsi dan nilai kembalinya, di mana siapa pun dapat mencari panggilan fungsi dan (jika itu dalam DB) menggunakan nilai kembali yang disediakan oleh seseorang di sisi lain dari dunia yang menggunakan fungsi identik bertahun-tahun lalu pada proyek yang berbeda.

Warbo
sumber
4

Seperti yang tertulis, itu adalah fungsi murni. Tidak menghasilkan efek samping. Fungsi ini memiliki satu parameter formal, tetapi memiliki dua input, dan akan selalu menampilkan nilai yang sama untuk dua input apa pun.

11112222233333
sumber
2

Bisakah kita memanggil fungsi seperti fungsi murni. Jika jawabannya TIDAK, bagaimana kita bisa menolaknya menjadi satu?

Seperti yang telah Anda catat, "itu mungkin memberi saya hasil yang berbeda besok" . Jika itu masalahnya, jawabannya akan tegas "tidak" . Ini terutama terjadi jika perilaku yang Anda maksudkan dollarToEurotelah ditafsirkan dengan benar sebagai:

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

Namun, ada interpretasi yang berbeda, di mana itu akan dianggap murni:

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

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

dollarToEuro langsung di atas murni.


Dari perspektif rekayasa perangkat lunak, penting untuk menyatakan ketergantungan dollarToEuropada fungsi fetchFromDatabase. Oleh karena itu, refactor definisi dollarToEurosebagai berikut:

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

Dengan hasil ini, mengingat premis yang fetchFromDatabaseberfungsi memuaskan, maka dapat disimpulkan bahwa proyeksi fetchFromDatabaseon dollarToEuroharus memuaskan. Atau pernyataan " fetchFromDatabasemurni" menyiratkan dollarToEuromurni (karena fetchFromDatabasemerupakan dasar untuk dollarToEurooleh faktor skalar dari x.

Dari posting asli, saya bisa mengerti itu fetchFromDatabaseadalah waktu fungsi. Mari kita tingkatkan upaya refactoring untuk membuat pemahaman itu transparan, karenanya jelas memenuhi syarat fetchFromDatabasesebagai fungsi murni:

fetchFromDatabase = (timestamp) => {/ * begini implementasinya * /};

Pada akhirnya, saya akan memperbaiki fitur sebagai berikut:

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();

Akibatnya, dollarToEurodapat diuji unit hanya dengan membuktikan bahwa ia memanggil dengan benar fetchFromDatabase(atau turunannya exchangeRate).

Igwe Kalu
sumber
1
Ini sangat mencerahkan. +1. Terima kasih.
Snowman
Sementara saya menemukan jawaban Anda lebih informatif, dan mungkin refactoring yang lebih baik untuk kasus penggunaan khusus dollarToEuro; Saya sudah menyebutkan di OP bahwa mungkin ada kasus penggunaan lain. Saya memilih dollarToEuro karena langsung membangkitkan apa yang saya coba lakukan, tetapi mungkin ada sesuatu yang kurang halus yang tergantung pada variabel bebas yang dapat berubah, tetapi tidak harus sebagai fungsi waktu. Dengan mengingat hal itu, saya menemukan bahwa refactor yang terpilih adalah yang lebih mudah diakses dan yang dapat membantu orang lain dengan kasus penggunaan yang serupa. Bagaimanapun, terima kasih atas bantuan Anda.
Snowman
-1

Saya seorang bilingual Haskell / JS dan Haskell adalah salah satu bahasa yang membuat masalah besar tentang kemurnian fungsi, jadi saya pikir saya akan memberi Anda perspektif dari bagaimana Haskell melihatnya.

Seperti yang dikatakan orang lain, di Haskell, membaca variabel yang bisa berubah umumnya dianggap tidak murni. Ada perbedaan antara variabel dan definisi bahwa variabel dapat berubah nanti, definisi adalah sama selamanya. Jadi jika Anda telah mendeklarasikannya const(dengan asumsi itu hanya sebuah numberdan tidak memiliki struktur internal yang bisa berubah), membaca dari itu akan menggunakan definisi, yang murni. Tetapi Anda ingin memodelkan nilai tukar yang berubah dari waktu ke waktu, dan itu membutuhkan semacam ketidakstabilan dan kemudian Anda menjadi kotor.

Untuk menggambarkan hal-hal tidak murni semacam itu (kita bisa menyebutnya "efek", dan penggunaannya "efektif" sebagai lawan "murni") di Haskell, kami melakukan apa yang Anda sebut metaprogramming . Hari ini metaprogramming biasanya mengacu pada makro yang bukan apa yang saya maksudkan, tetapi lebih kepada gagasan menulis suatu program untuk menulis program lain secara umum.

Dalam hal ini, di Haskell, kita menulis perhitungan murni yang menghitung program yang efektif yang kemudian akan melakukan apa yang kita inginkan. Jadi seluruh poin dari file sumber Haskell (setidaknya, yang menggambarkan sebuah program, bukan perpustakaan) adalah untuk menggambarkan perhitungan murni untuk program yang efektif yang menghasilkan-kekosongan, yang disebut main. Maka tugas kompiler Haskell adalah mengambil file sumber ini, melakukan perhitungan murni itu, dan meletakkan program yang efektif sebagai biner yang dapat dieksekusi di suatu tempat di hard drive Anda untuk dijalankan kemudian di waktu luang Anda. Ada celah, dengan kata lain, antara waktu ketika komputasi murni berjalan (sementara kompiler membuat executable) dan waktu ketika program yang efektif berjalan (setiap kali Anda menjalankan executable).

Jadi bagi kami, program yang efektif adalah benar-benar struktur data dan mereka secara intrinsik tidak melakukan apa-apa hanya dengan disebutkan (mereka tidak memiliki efek * samping- * selain nilai pengembalian mereka; nilai pengembaliannya mengandung efeknya). Untuk contoh kelas TypeScript yang sangat ringan yang menjelaskan program yang tidak dapat diubah dan beberapa hal yang dapat Anda lakukan dengannya,

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()));
   }
}

Kuncinya adalah bahwa jika Anda memiliki Program<x>efek samping maka tidak terjadi dan ini benar-benar murni entitas fungsional. Memetakan fungsi melalui program tidak memiliki efek samping apa pun kecuali fungsi itu bukan fungsi murni; mengurutkan dua program tidak memiliki efek samping; dll.

Jadi misalnya bagaimana menerapkan ini dalam kasus Anda, Anda dapat menulis beberapa fungsi murni yang mengembalikan program untuk mendapatkan pengguna dengan ID dan untuk mengubah database dan mengambil data JSON, seperti

// 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()));
}

dan kemudian Anda bisa mendeskripsikan pekerjaan cron untuk meringkuk URL dan mencari karyawan dan memberi tahu atasan mereka dengan cara yang berfungsi murni sebagai

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!'
          ))
    );

Intinya adalah bahwa setiap fungsi di sini adalah fungsi yang sepenuhnya murni; tidak ada yang benar-benar terjadi sampai saya benar action.run()- benar menggerakkannya. Selain itu saya dapat menulis fungsi seperti,

// 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()]));
}

dan jika JS memiliki janji pembatalan kami dapat memiliki dua program saling berlomba dan mengambil hasil pertama dan membatalkan yang kedua. (Maksudku, kita masih bisa, tetapi menjadi kurang jelas apa yang harus dilakukan.)

Demikian pula dalam kasus Anda, kami dapat menjelaskan perubahan nilai tukar

declare const exchangeRate: Program<number>;

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

dan exchangeRatebisa menjadi program yang melihat nilai yang bisa berubah,

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); 
});

tetapi meskipun demikian, fungsi dollarsToEurosini sekarang merupakan fungsi murni dari angka ke program yang menghasilkan angka, dan Anda dapat mempertimbangkannya dengan cara yang sama dan deterministik sehingga Anda dapat mempertimbangkan tentang program apa pun yang tidak memiliki efek samping.

Biaya, tentu saja, adalah bahwa Anda harus akhirnya memanggilnya di .run() suatu tempat , dan itu tidak murni. Tetapi seluruh struktur perhitungan Anda dapat dijelaskan dengan perhitungan murni, dan Anda dapat mendorong pengotor ke margin kode Anda.

CR Drost
sumber
Saya ingin tahu mengapa ini terus diturunkan tetapi saya maksudnya saya masih bertahan (itu, sebenarnya, bagaimana Anda memanipulasi program di Haskell di mana semuanya murni secara default) dan dengan senang hati akan menahan downvotes. Namun, jika downvoters ingin meninggalkan komentar menjelaskan apa yang tidak mereka sukai tentang itu, saya dapat mencoba memperbaikinya.
CR Drost
Ya, saya bertanya-tanya mengapa ada begitu banyak downvotes tetapi tidak ada satu komentar, selain tentu saja penulis.
Buda Örs