Bayangkan sebuah function yang hari ini menghasilkan 55500 untuk harga tertentu, tetapi besok menghasilkan 100000 untuk input yang sama persis. Bug semacam ini hampir selalu bersumber dari function yang diam-diam membaca atau mengubah state di luar dirinya. Pure function menutup celah tersebut lewat dua janji sederhana: hasil yang konsisten untuk input yang sama, dan nol efek samping.
Dua Aturan Pure Function #
Sebuah function disebut pure apabila memenuhi dua aturan berikut:
- Input sama, hasil selalu sama — nilai balik hanya dihitung dari argument yang diterima, tanpa membaca variabel global, jam sistem, atau isi file.
- Tidak ada side effect — function tidak mengubah apa pun di luar dirinya: tidak menulis variabel global, tidak memodifikasi list milik pemanggil, tidak mencetak, tidak menulis ke file.
Mari kita lihat function yang memenuhi kedua aturan tersebut:
def harga_akhir(harga, pajak=0.11):
return round(harga * (1 + pajak))
print(harga_akhir(50000))
print(harga_akhir(50000))
print(harga_akhir(50000) == harga_akhir(50000))Output yang Dihasilkan:
55500
55500
TrueMari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 1-2 (
def harga_akhir...): seluruh perhitungan hanya melibatkan dua parameter lokal,hargadanpajak. Function tidak membaca variabel lain, sehingga hasilnya sepenuhnya ditentukan oleh argument. - Baris 4-5 (
print(harga_akhir(50000))): dua pemanggilan dengan argument identik menghasilkan angka identik —55500keduanya. - Baris 6 (
print(... == ...)): perbandingan mengembalikanTrue, menegaskan sifat deterministik function tersebut.
Lawannya: Impure Function #
Function berikut melanggar kedua aturan sekaligus. Function membaca sekaligus menaikkan variabel global total_terjual — pola global yang sudah kita bahas pada Chapter local, global, dan nonlocal:
total_terjual = 0
def catat_penjualan(jumlah):
global total_terjual
total_terjual += jumlah
return total_terjual
print(catat_penjualan(50000))
print(catat_penjualan(50000))Output yang Dihasilkan:
50000
100000Dua pemanggilan dengan argument 50000 menghasilkan dua angka berbeda karena hasil bergantung pada berapa kali function dipanggil sebelumnya. Inilah ciri impure function: hasilnya tidak bisa dipahami dari argument saja.
| Kriteria | Pure function | Impure function |
|---|---|---|
| Hasil untuk argument sama | Selalu identik | Bisa berbeda antar pemanggilan |
| Ketergantungan | Hanya argument | Argument + state luar (global, file, waktu) |
| Efek samping | Tidak ada | Ada (mutasi global, print, tulis file) |
| Urutan pemanggilan | Bebas ditukar | Memengaruhi hasil akhir |
| Pengujian | Panggil, bandingkan hasil | Perlu menyiapkan lalu mereset state |
Kenapa Kita Ingin Function Murni #
Tiga manfaat utama membuat pure function layak dikejar:
- Mudah diuji — tidak ada state yang harus disiapkan atau dibersihkan; cukup panggil dan bandingkan hasil dengan nilai harapan.
- Bisa di-cache — karena input sama selalu menghasilkan output sama, hasil lama boleh dipakai ulang tanpa takut basi.
- Aman diparalelkan — function yang tidak menyentuh memori bersama tidak akan saling menginjak saat dijalankan di beberapa thread.
Bukti bahwa pure function bisa di-cache terlihat pada decorator lru_cache dari module functools:
from functools import lru_cache
@lru_cache(maxsize=None)
def harga_grosir(jumlah):
print(f"menghitung untuk {jumlah} unit...")
if jumlah >= 100:
return 9000
if jumlah >= 10:
return 9500
return 10000
print(harga_grosir(50))
print(harga_grosir(50))Output yang Dihasilkan:
menghitung untuk 50 unit...
9500
9500Baris menghitung untuk 50 unit... hanya muncul sekali. Pemanggilan kedua dilayani dari cache — lru_cache tidak menjalankan ulang badan function. Perhatikan pula bahwa harga_grosir melanggar aturan kemurnian sedikit karena memanggil print; pada praktiknya kita menganggap print untuk jejak debug sebagai pengecualian yang bisa diterima.
Praktiknya: Functional Core, Imperative Shell #
Program nyata tetap harus mencetak, membaca file, dan mengakses database — kegiatan yang tidak murni. Strategi pragmatisnya adalah menekan semua I/O ke tepi program (imperative shell) dan menjaga inti perhitungan tetap murni (functional core):
def hitung_diskon(total, persen):
return round(total * persen / 100)
def ringkas_pesanan(items):
total = sum(items)
diskon = hitung_diskon(total, 10)
return {"total": total, "diskon": diskon, "bayar": total - diskon}
def tampilkan_struk(pesanan):
print(f"Total : Rp{pesanan['total']}")
print(f"Diskon : Rp{pesanan['diskon']}")
print(f"Bayar : Rp{pesanan['bayar']}")
tampilkan_struk(ringkas_pesanan([15000, 22500, 8000]))Output yang Dihasilkan:
Total : Rp45500
Diskon : Rp4550
Bayar : Rp40950Function hitung_diskon dan ringkas_pesanan membentuk core murni: keduanya hanya menghitung dari input dan mengembalikan dictionary baru yang dibuat lokal. Function tampilkan_struk adalah shell — satu-satunya tempat print terjadi. Kemurnian memang sebuah spektrum; semakin besar porsi core yang murni, semakin besar bagian program yang mudah diuji dan dipercaya.