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:

  1. Input sama, hasil selalu sama — nilai balik hanya dihitung dari argument yang diterima, tanpa membaca variabel global, jam sistem, atau isi file.
  2. 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:

python
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:

text
55500
55500
True

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 1-2 (def harga_akhir...): seluruh perhitungan hanya melibatkan dua parameter lokal, harga dan pajak. Function tidak membaca variabel lain, sehingga hasilnya sepenuhnya ditentukan oleh argument.
  2. Baris 4-5 (print(harga_akhir(50000))): dua pemanggilan dengan argument identik menghasilkan angka identik — 55500 keduanya.
  3. Baris 6 (print(... == ...)): perbandingan mengembalikan True, 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:

python
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:

text
50000
100000

Dua 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.

KriteriaPure functionImpure function
Hasil untuk argument samaSelalu identikBisa berbeda antar pemanggilan
KetergantunganHanya argumentArgument + state luar (global, file, waktu)
Efek sampingTidak adaAda (mutasi global, print, tulis file)
Urutan pemanggilanBebas ditukarMemengaruhi hasil akhir
PengujianPanggil, bandingkan hasilPerlu 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:

python
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:

text
menghitung untuk 50 unit...
9500
9500

Baris 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):

python
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:

text
Total  : Rp45500
Diskon : Rp4550
Bayar  : Rp40950

Function 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.