Siti mengelola pendaftaran lokakarya dan memakai satu function untuk menghitung biaya. Laporan dari rekan tim membingungkan: form yang sama dikirim dua kali, tetapi total yang muncul berbeda. Setelah ditelusuri, function tersebut membaca kode diskon dari variabel di luarnya lalu mengosongkannya setiap kali selesai dipakai — pemanggilan pertama mendapat potongan, pemanggilan kedua tidak. Tidak ada yang salah dengan operasi aritmetikanya; yang bermasalah adalah ketergantungan function pada keadaan di luar dirinya.

Function murni (pure function) adalah kontrak yang menuntaskan masalah semacam itu. Kontraknya cukup dua kalimat: keluaran ditentukan sepenuhnya oleh masukan, dan function tidak mengubah apa pun di luar dirinya. Chapter ini membuktikan kedua syarat itu dengan kode yang bisa dijalankan — membandingkan function yang tidak murni dengan versi murninya, lalu menguji bahwa hasilnya tetap sama berapa kali pun dipanggil.

Dua Syarat Function Murni #

Sebuah function disebut murni bila memenuhi dua syarat sekaligus:

  • Deterministik: masukan sama selalu menghasilkan keluaran sama, kapan pun dan di mana pun dipanggil.
  • Tanpa side effect: function tidak membaca maupun mengubah keadaan di luarnya — variabel global, data milik pemanggil, atau perangkat keluaran seperti terminal.

Mekanisme bahasanya perlu dipahami secara jujur: JavaScript tidak melarang function menyentuh variabel luar. Lexical scope dari section Functions memberi setiap function akses ke seluruh lingkaran variabel di atasnya, sehingga kemurnian adalah disiplin yang kita pilih sendiri, bukan aturan yang ditegaskan engine. Pilihan itu berharga karena function murni bisa diuji tanpa menyiapkan keadaan apa pun, bisa dipindahkan ke bagian lain program tanpa risiko tersembunyi, dan hasilnya bisa dipercaya oleh bagian program mana pun yang memanggilnya.

Karena tidak ada penegakan otomatis, kemurnian perlu dibaca dari gejalanya. Tiga sinyal yang paling sering muncul pada function yang tidak murni: nama variabel di luarnya ikut tertulis di dalam badan function, data yang diterima sebagai argumen ikut diubah, dan hasilnya berbeda walau argumennya sama. Tidak semua sinyal berarti pasti bermasalah, tetapi ketiganya layak diselidiki setiap kali ditemui.

Dua Function yang Tidak Murni #

Berkas berikut memuat dua eksperimen impure, satu versi murni, dan satu uji deterministik. Dua eksperimen pertama sengaja memperlihatkan pelanggaran pada tiap syarat — yang pertama bergantung pada keadaan luar, yang kedua mengubah data milik pemanggil:

// fungsi-murni.js
// Pure function: keluaran sama untuk masukan sama, tanpa side effect.

// --- 1. impure: membaca dan mengubah kondisi di luar parameternya ---
let kodeDiskon = "LOKAKARYA10";

function hitungBiayaImpure(hargaDasar) {
  if (kodeDiskon === "LOKAKARYA10") {
    hargaDasar = hargaDasar - (hargaDasar * 10) / 100;
  }
  kodeDiskon = null; // side effect: keadaan program ikut berubah
  return hargaDasar;
}

console.log(hitungBiayaImpure(200000));
console.log(hitungBiayaImpure(200000));
console.log(kodeDiskon);

// --- 2. impure: mengubah data milik pemanggil ---
const pendaftar = ["[email protected]", "[email protected]"];

function tandaiTerdaftar(daftar, email) {
  daftar.push(email); // side effect: array milik pemanggil ikut berubah
  return daftar.length;
}

console.log(tandaiTerdaftar(pendaftar, "[email protected]"));
console.log(pendaftar.length);

// --- 3. pure: semua masukan lewat parameter, keluaran lewat return ---
function hitungBiaya(hargaDasar, kode) {
  if (kode === "LOKAKARYA10") {
    return hargaDasar - (hargaDasar * 10) / 100;
  }
  return hargaDasar;
}

console.log(hitungBiaya(200000, "LOKAKARYA10"));
console.log(hitungBiaya(200000, null));

// --- 4. uji deterministik: masukan sama, hasil sama, berapa kali pun ---
const hasilPertama = hitungBiaya(200000, "LOKAKARYA10");
const hasilKedua = hitungBiaya(200000, "LOKAKARYA10");
console.log(hasilPertama === hasilKedua);
console.log(
  hitungBiaya(150000, "LOKAKARYA10") === hitungBiaya(150000, "LOKAKARYA10")
);

Output yang Dihasilkan:

text
180000
200000
null
3
3
180000
200000
true
true

Mari kita telaah baris-baris pentingnya:

  1. Baris 5–13 (hitungBiayaImpure): function ini membaca kodeDiskon — variabel di luar parameternya — untuk memutuskan diskon, lalu mengosongkannya di baris 11. Rumus diskon di baris 9 ditulis harga - (harga × 10) / 100 agar hasilnya tetap bilangan bulat, bukan pecahan floating point.
  2. Baris 15–16 (dua pemanggilan identik): panggilan pertama menghasilkan 180000 karena kodeDiskon masih terisi; panggilan kedua dengan argumen sama menghasilkan 200000 karena kodenya sudah jadi null. Masukan sama, keluaran berbeda — syarat deterministik dilanggar.
  3. Baris 17 (console.log(kodeDiskon)): bukti side effect-nya. Variabel global kini bernilai null — keadaan seluruh program berubah hanya karena sebuah function “menghitung biaya”.
  4. Baris 20–28 (tandaiTerdaftar): pelanggaran kedua. Function menerima array pendaftar, lalu push email baru langsung ke dalamnya di baris 23. Kembalian 3 memang sesuai harapan, tetapi data milik pemanggil ikut bertambah — baris 28 membuktikannya lewat length yang kini 3, bukan 2.
  5. Baris 31–36 (hitungBiaya): versi murninya. Seluruh masukan datang lewat parameter (hargaDasar dan kode), seluruh keluaran lewat return. Tidak ada variabel luar yang dibaca, ditulis, atau diubah isinya.
  6. Baris 38–39 (pemanggilan versi murni): masukan yang sama dengan eksperimen pertama kini stabil — 180000 selama kode diskon dikirim sebagai argumen, 200000 selama tidak. Tidak ada pemanggilan yang memengaruhi pemanggilan berikutnya.

Menguji Hasil Sama Berapa Kali Pun #

Deterministik bukan sekadar janji di dokumen, melainkan sifat yang bisa diuji. Eksperimen terakhir pada berkas memanggil hitungBiaya dua kali dengan argumen identik, lalu membandingkan kedua kembaliannya dengan === — keduanya true. Pembandingan semacam itu bisa diperluas ke sebanyak pemanggilan yang kita mau:

js
const pengulangan = [1, 2, 3, 4, 5];
const semuaSama = pengulangan.every(
  () => hitungBiaya(250000, "LOKAKARYA10") === 225000
);
console.log(semuaSama);

Output yang Dihasilkan:

text
true

Method every dari section Arrays menjalankan callback lima kali, dan kelima pemanggilan itu menghasilkan 225000 — nilai 250000 dikurangi sepuluh persen. Inilah wajah pengujian function murni: tidak ada keadaan yang perlu disiapkan, tidak ada urutan pemanggilan yang perlu dijaga; cukup masukan masuk dan keluaran dibandingkan.

Dibandingkan menguji function yang bergantung keadaan luar, perbedaannya terasa pada persiapannya. Untuk hitungBiayaImpure, pengujian harus menata ulang kodeDiskon sebelum setiap kasus, lalu mengembalikannya setelahnya — dan bila ada test lain yang lupa merapikan, test berikutnya mewarisi keadaan yang sudah rusak. Function murni memotong seluruh rutinitas itu dari akarnya.

Batas Membaca Keadaan Luar #

Satu pertanyaan yang wajar muncul: bolehkah function murni membaca apa pun di luarnya? Aturan paling ketat menjawab tidak — seluruh masukan harus lewat parameter. Dalam praktik, konstanta yang memang tidak pernah berubah, seperti nama aplikasi atau batas usia minimum, biasanya masih dianggap wajar dibaca dari luarnya karena hasilnya tidak bisa berubah di tengah eksekusi.

Yang benar-benar perlu dijauhi adalah membaca keadaan yang bisa berubah: penghitung, tanda status, isi array yang diperbarui bagian lain program. Keadaan semacam itulah yang membuat keluaran berubah tanpa argumen ikut berubah — persis insiden Siti di pembuka chapter ini. Bila function membutuhkan nilai dari keadaan semacam itu, kirimkan sebagai parameter; keputusan pemanggilan tetap di tangan pemanggil, dan function tetap deterministik.

Manfaat yang Ikut Datang #

Kontrak dua kalimat tadi membawa sejumlah konsekuensi praktis:

  • Mudah diuji: test cukup memberi argumen dan membandingkan hasil, tanpa menyiapkan data luar.
  • Aman dirangkai: keluaran satu function bisa langsung dipakai function lain tanpa takut keadaan tersembunyi berubah — fondasi composition yang dibahas dua chapter lagi.
  • Bisa dikenang (cache): karena masukan sama selalu menghasilkan keluaran sama, hasil boleh disimpan dan dipakai ulang — mekanisme memoization yang dibuktikan di chapter Closures in Practice.

Function murni berjanji tidak menyentuh data siapa pun. Agar janji itu mudah ditepati, chapter berikutnya melatih keterampilan pendampingnya: immutability — memperbarui data lewat salinan, bukan lewat mutasi.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

fungsi-murni.js

Unduh berkas
javascript
// fungsi-murni.js
// Pure function: keluaran sama untuk masukan sama, tanpa side effect.

// --- 1. impure: membaca dan mengubah kondisi di luar parameternya ---
let kodeDiskon = "LOKAKARYA10";

function hitungBiayaImpure(hargaDasar) {
  if (kodeDiskon === "LOKAKARYA10") {
    hargaDasar = hargaDasar - (hargaDasar * 10) / 100;
  }
  kodeDiskon = null; // side effect: keadaan program ikut berubah
  return hargaDasar;
}

console.log(hitungBiayaImpure(200000));
console.log(hitungBiayaImpure(200000));
console.log(kodeDiskon);

// --- 2. impure: mengubah data milik pemanggil ---
const pendaftar = ["[email protected]", "[email protected]"];

function tandaiTerdaftar(daftar, email) {
  daftar.push(email); // side effect: array milik pemanggil ikut berubah
  return daftar.length;
}

console.log(tandaiTerdaftar(pendaftar, "[email protected]"));
console.log(pendaftar.length);

// --- 3. pure: semua masukan lewat parameter, keluaran lewat return ---
function hitungBiaya(hargaDasar, kode) {
  if (kode === "LOKAKARYA10") {
    return hargaDasar - (hargaDasar * 10) / 100;
  }
  return hargaDasar;
}

console.log(hitungBiaya(200000, "LOKAKARYA10"));
console.log(hitungBiaya(200000, null));

// --- 4. uji deterministik: masukan sama, hasil sama, berapa kali pun ---
const hasilPertama = hitungBiaya(200000, "LOKAKARYA10");
const hasilKedua = hitungBiaya(200000, "LOKAKARYA10");
console.log(hasilPertama === hasilKedua);
console.log(
  hitungBiaya(150000, "LOKAKARYA10") === hitungBiaya(150000, "LOKAKARYA10")
);