Bayangkan sedang menelusuri bug: method yang Anda panggil dengan masukan yang sama memberi hasil berbeda antara percobaan pertama dan kedua. Penyebabnya hampir selalu sama — method tersebut menyimpan atau mengubah sesuatu di luar dirinya, sehingga perilakunya dipengaruhi sejarah pemanggilan sebelumnya. Dunia pemrograman fungsional memberi nama untuk penawarnya: pure function — method yang hasilnya sepenuhnya ditentukan masukannya dan tidak menimbulkan efek apa pun di luar dirinya. Java tidak memaksa kemurnian, tetapi menulis method inti secara murni membuat program jauh lebih mudah diuji, dibaca, dan diprediksi. Pada Chapter ini kita bandingkan langsung method murni dengan method yang bercampur state tersembunyi.

Dua Syarat Pure Function #

Sebuah method disebut pure bila memenuhi dua syarat sekaligus. Pertama, hasil hanya bergantung pada argument: masukan sama menghasilkan keluaran sama, kapan pun dan dalam urutan apa pun method dipanggil. Kedua, tanpa side effect: method tidak mengubah variabel di luar dirinya, tidak menulis berkas, tidak mencetak ke terminal, dan tidak menyentuh state program lainnya. Method seperti itu ibarat mesin hitung — ditekan tombol yang sama, keluar angka yang sama, dan mesinnya tidak mengingat apa pun tentang penekanan sebelumnya.

Mari kita mulai dari versi yang murni:

java FungsiMurni.java
public class FungsiMurni {

    // Pure: hasil hanya dari argument, tidak menyentuh apa pun di luar
    static double hitungPajak(double harga, double tarifPersen) {
        return harga * tarifPersen / 100.0;
    }

    static String label(String nama, int nilai) {
        return nama + " (" + nilai + ")";
    }

    public static void main(String[] args) {
        System.out.println(hitungPajak(100000, 11));
        System.out.println(hitungPajak(100000, 11));   // hasil identik

        System.out.println(label("Budi", 90));
        System.out.println(label("Budi", 90));          // hasil identik
    }
}

Output yang Dihasilkan:

text
11000.0
11000.0
Budi (90)
Budi (90)

Kedua method hanya membaca parameter dan mengembalikan hasil perhitungan atau rangkaian teks. Pemanggilan kedua hitungPajak(100000, 11) menghasilkan 11000.0 yang persis sama dengan pemanggilan pertama, dan pemanggilan kedua label("Budi", 90) tetap menghasilkan Budi (90) — tidak ada yang berubah karena memang tidak ada yang bisa berubah. Pustaka standar Java kaya akan method semacam ini: seluruh method Math (misalnya Math.abs, Math.max) bersifat murni, demikian pula operasi pada String seperti toUpperCase() yang mengembalikan String baru tanpa mengubah yang lama.

Wajah Method Bercampur #

Sekarang kita lihat kebalikannya. Program berikut memelihara dua variabel static sebagai state bersama, lalu menyentuhnya dari dalam method:

public class FungsiBercampur {

    // State bersama di luar method: sumber ketidakmurnian
    static int nomorUrut = 0;
    static double totalPajak = 0;

    // Impure: hasil bergantung pada state tersembunyi, bukan hanya argument
    static int panggilanBerikutnya() {
        nomorUrut = nomorUrut + 1;
        return nomorUrut;
    }

    // Impure: nilai balik tetap, tetapi mengubah state di luar dirinya
    static double catatPajak(double harga, double tarifPersen) {
        double pajak = harga * tarifPersen / 100.0;
        totalPajak = totalPajak + pajak;   // side effect
        return pajak;
    }

    public static void main(String[] args) {
        System.out.println("panggilanBerikutnya() = " + panggilanBerikutnya());
        System.out.println("panggilanBerikutnya() = " + panggilanBerikutnya());

        System.out.println("catatPajak(100000, 11) = " + catatPajak(100000, 11));
        System.out.println("catatPajak(100000, 11) = " + catatPajak(100000, 11));
        System.out.println("totalPajak             = " + totalPajak);
    }
}

Output yang Dihasilkan:

text
panggilanBerikutnya() = 1
panggilanBerikutnya() = 2
catatPajak(100000, 11) = 11000.0
catatPajak(100000, 11) = 11000.0
totalPajak             = 22000.0

Keluaran program tersebut memperlihatkan dua wajah ketidakmurnian. Method panggilanBerikutnya mematahkan syarat pertama: dua pemanggilan tanpa argument sama sekali menghasilkan 1 lalu 2, karena hasilnya ditentukan state tersembunyi nomorUrut, bukan masukan. Method catatPajak lebih halus — nilai baliknya konsisten 11000.0 di kedua pemanggilan, sehingga terlihat seperti murni, tetapi setiap pemanggilan menambah totalPajak hingga terakumulasi 22000.0: efek samping yang hanya terlihat bila kita membaca state tersebut. Perbandingannya dapat dirangkum begini:

AspekMethod pureMethod impure
Hasil dari masukan samaSelalu samaBisa berbeda (panggilanBerikutnya)
Perubahan state luarTidak adaAda (catatPajak menambah totalPajak)
Aman dipanggil ulangYaTergantung efek yang ditimbulkan
Penelusuran bugLokal pada methodHarus membaca seluruh riwayat pemanggilan

Mengapa Pure Function Disukai #

Kemurnian membayar dirinya sendiri dalam tiga mata uang. Pengujian: method murni diuji tanpa persiapan — panggil dengan masukan, bandingkan keluaran, selesai; tidak perlu mereset state atau mengurutkan skenario. Penalaran: untuk memahami method murni, cukup baca badannya — seluruh kebenarannya ada di satu tempat, tidak tersebar ke variabel global. Keselamatan: method murni aman dipanggil dari beberapa thread sekaligus karena tidak memperebutkan state bersama — beban berpikir concurrency jauh berkurang.

Program nyata tetap membutuhkan efek: mencetak laporan, menyimpan berkas, mengirim data. Strategi praktisnya bukan menghapus efek, melainkan mengekangnya: susun program agar logika inti (menghitung, memvalidasi, mentransformasikan data) berupa method murni, lalu kumpulkan pemanggilan efek pada tempat sempit di tepi program. main pada FungsiMurni.java adalah contoh pola ini — method hitungPajak dan label murni, sementara System.out.println sebagai efek diletakkan di pemanggil. Pemisahan semacam ini akan kita praktikkan penuh pada Chapter penutup Section: merombak satu program monolitik menjadi susunan method kecil yang murni dan teruji.

Referensi #

Berkas Sumber

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

FungsiBercampur.java

Unduh berkas
java
public class FungsiBercampur {

    // State bersama di luar method: sumber ketidakmurnian
    static int nomorUrut = 0;
    static double totalPajak = 0;

    // Impure: hasil bergantung pada state tersembunyi, bukan hanya argument
    static int panggilanBerikutnya() {
        nomorUrut = nomorUrut + 1;
        return nomorUrut;
    }

    // Impure: nilai balik tetap, tetapi mengubah state di luar dirinya
    static double catatPajak(double harga, double tarifPersen) {
        double pajak = harga * tarifPersen / 100.0;
        totalPajak = totalPajak + pajak;   // side effect
        return pajak;
    }

    public static void main(String[] args) {
        System.out.println("panggilanBerikutnya() = " + panggilanBerikutnya());
        System.out.println("panggilanBerikutnya() = " + panggilanBerikutnya());

        System.out.println("catatPajak(100000, 11) = " + catatPajak(100000, 11));
        System.out.println("catatPajak(100000, 11) = " + catatPajak(100000, 11));
        System.out.println("totalPajak             = " + totalPajak);
    }
}

FungsiMurni.java

Unduh berkas
java
public class FungsiMurni {

    // Pure: hasil hanya dari argument, tidak menyentuh apa pun di luar
    static double hitungPajak(double harga, double tarifPersen) {
        return harga * tarifPersen / 100.0;
    }

    static String label(String nama, int nilai) {
        return nama + " (" + nilai + ")";
    }

    public static void main(String[] args) {
        System.out.println(hitungPajak(100000, 11));
        System.out.println(hitungPajak(100000, 11));   // hasil identik

        System.out.println(label("Budi", 90));
        System.out.println(label("Budi", 90));          // hasil identik
    }
}