Kemurnian dan imutabilitas

Transparansi referensial dan penalaran substitusi untuk program yang mudah diuji.

Daftar isi

Fungsi murni (pure function) adalah fungsi yang keluarannya ditentukan hanya oleh masukannya dan tidak menimbulkan efek samping pada lingkup di luarnya. Topik ini membahas definisi kemurnian tersebut, transparansi referensial beserta uji substitusi yang mengikutinya, kontras dengan fungsi yang membaca keadaan global dan waktu sistem, serta imutabilitas sebagai pendamping kemurnian. Hasil yang diperoleh adalah kemampuan menilai kebenaran program melalui penalaran substitusi dan menyusun fungsi yang dapat diuji tanpa menyiapkan keadaan tersembunyi.

Fungsi murni#

Kemurnian dinyatakan dengan dua syarat. Pertama, keluaran hanya bergantung pada masukannya, sehingga fungsi tidak membaca variabel global, waktu sistem, berkas, maupun sumber lain yang berubah di luar kendalinya. Kedua, fungsi tidak menimbulkan efek samping, yaitu tidak mengubah variabel di luarnya, tidak menulis berkas, dan tidak melakukan interaksi lain yang terlihat dari luar. Seluruh komunikasi fungsi murni dengan sekitarnya berlangsung melalui parameter dan nilai kembalian, sehingga perilakunya dapat dipahami dari tanda tangannya saja.

Program berikut memuat fungsi potongan yang murni. Nilai potongan ditentukan sepenuhnya oleh harga yang diterima, dan fungsi tersebut tidak menyentuh variabel apa pun di luarnya.

scala diskon.scala
// Fungsi murni: keluaran hanya ditentukan oleh masukannya
@main def hitungTotal(): Unit =
  def potongan(harga: Int): Int =
    if harga >= 100000 then harga / 10 else 0

  val total: Int = 120000 - potongan(120000)

  println(potongan(120000))
  println(total)
bash
# menjalankan program diskon.scala
scala-cli run diskon.scala
text Keluaran
12000
108000

Fungsi potongan menghasilkan 12000 untuk harga 120000 karena harga tersebut memenuhi syarat potongan sepuluh persen, dan total bayar menjadi 108000. Tabel berikut merangkum perbedaan fungsi murni dan fungsi yang tidak murni menurut sumber keluarannya.

CiriFungsi murniFungsi tidak murni
Sumber keluaranMasukan sajaMasukan ditambah keadaan global atau waktu sistem
Efek sampingTidak adaMengubah variabel luar, menulis berkas, atau mencatat waktu
Pemanggilan ulangSelalu menghasilkan nilai yang samaDapat menghasilkan nilai berbeda pada setiap pemanggilan

Transparansi referensial dan uji substitusi#

Transparansi referensial (referential transparency) adalah sifat sebuah ekspresi yang dapat diganti dengan nilai kembaliannya tanpa mengubah perilaku program. Sifat ini dimiliki seluruh ekspresi yang dibangun dari fungsi murni. Uji substitusi memanfaatkan sifat tersebut sebagai cara penalaran, yaitu mengganti setiap pemanggilan fungsi dengan hasil yang dijanjikannya, lalu memastikan sisa program tetap berperilaku sama.

Program berikut membandingkan dua perhitungan total. Nilai a dihitung dengan memanggil diskon, sedangkan nilai b menuliskan hasil pemanggilan itu secara langsung.

scala substitusi.scala
// Uji substitusi: pemanggilan diganti nilai kembaliannya tanpa mengubah perilaku
@main def substitusi(): Unit =
  def diskon(harga: Int): Int =
    if harga >= 100000 then harga / 10 else 0

  val a: Int = 120000 - diskon(120000)
  val b: Int = 120000 - 12000

  println(a)
  println(b)
  println(a == b)
bash
# menjalankan program substitusi.scala
scala-cli run substitusi.scala
text Keluaran
108000
108000
true

Keluaran memperlihatkan a dan b bernilai sama karena 120000 - diskon(120000) dan 120000 - 12000 tidak dapat dibedakan dari luar. Penggantian semacam ini sah hanya selama fungsi yang dipanggil tetap murni. Apabila fungsi tersebut berubah menjadi tidak murni, nilai yang ditulis langsung tidak lagi mewakili pemanggilannya, sehingga substitusi mengubah perilaku program tanpa terdeteksi dan tanpa pesan error sama sekali.

Kontras dengan keadaan global dan waktu sistem#

Fungsi yang membaca keadaan global melanggar kedua syarat kemurnian sekaligus. Keluarannya tidak lagi ditentukan oleh masukan karena bergantung pada riwayat pemanggilan yang terjadi sebelumnya. Apabila fungsi itu juga mencatat waktu, keluarannya berubah pada setiap eksekusi dan menjadi tidak deterministik. Contoh berikut menggambarkan fungsi penomoran yang menyertakan penghitung global dan waktu sistem, dan sengaja ditampilkan tanpa blok Keluaran karena hasilnya memang tidak dapat dikunci.

scala Contoh: fungsi yang menyertakan keadaan global dan waktu sistem
import java.time.Instant

var penghitung: Int = 0

def beriNomor(isi: String): String =
  penghitung += 1
  s"${Instant.now()} nomor $penghitung, $isi"

Pada fungsi beriNomor, dua pemanggilan dengan masukan sama tidak lagi menjamin hasil yang sama karena penghitungnya bertambah pada setiap pemanggilan dan waktunya berjalan terus. Uji substitusi pun gagal diterapkan, sebab pemanggilan tidak dapat diganti dengan satu nilai tetap. Bentuk seperti ini tetap diperlukan di tempatnya, misalnya pada pencatatan log, tetapi batasnya perlu ditandai secara eksplisit dan ditekan pada lapisan luar program, sedangkan inti perhitungan tetap disusun dari fungsi murni.

Manfaat untuk pengujian dan konkurensi#

Fungsi murni menyederhanakan pengujian karena yang diperlukan hanyalah masukan dan keluaran yang diharapkan. Tidak ada keadaan global yang harus diatur sebelum tes dan direset setelahnya, dan kegagalan tes pasti berasal dari fungsi yang diuji, bukan dari urutan tes yang berjalan lebih dahulu. Fungsi murni juga aman dijalankan secara konkuren karena dua bagian program yang tidak mengubah data bersama tidak dapat saling mengganggu, sehingga urutan eksekusinya tidak memengaruhi hasil.

Program berikut memanggil fungsi diskon yang sama pada dua harga, lalu memanggilnya kembali untuk harga pertama setelah pemanggilan lain berjalan di antaranya.

scala cek-urutan.scala
// Hasil fungsi murni tidak bergantung pada urutan pemanggilan
@main def cekUrutan(): Unit =
  def diskon(harga: Int): Int =
    if harga >= 100000 then harga / 10 else 0

  val hargaA: Int = diskon(150000)
  val hargaB: Int = diskon(90000)
  val hargaAulang: Int = diskon(150000)

  println(hargaA)
  println(hargaB)
  println(hargaA == hargaAulang)
bash
# menjalankan program cek-urutan.scala
scala-cli run cek-urutan.scala
text Keluaran
15000
0
true

Keluaran memperlihatkan harga 90000 tidak menghasilkan potongan karena berada di bawah batasnya, sedangkan pemanggilan ulang untuk 150000 tetap menghasilkan 15000. Nilai true pada baris terakhir menegaskan bahwa pemanggilan di antara keduanya tidak menyisakan pengaruh apa pun. Pola inilah yang diandalkan pengujian karena urutan pemanggilan yang bebas membuat setiap kasus uji berdiri sendiri.

Imutabilitas sebagai pendamping kemurnian#

Fungsi murni umumnya bekerja pada data yang tidak berubah karena data yang stabil menjamin hubungan antara masukan dan keluaran tetap berlaku sepanjang waktu. Imutabilitas (immutability) ditegakkan melalui val yang melarang penugasan ulang, serta koleksi bawaan Scala yang immutable secara default sebagaimana dijelaskan pada modul koleksi. Perilaku map dan filter yang selalu menghasilkan koleksi baru merupakan penerapan langsung dari prinsip ini.

scala koleksi-utuh.scala
// Koleksi asal tetap utuh karena operasi menghasilkan koleksi baru
@main def koleksiUtuh(): Unit =
  val dasar: List[Int] = List(1, 2, 3)

  val diperluas: List[Int] = dasar :+ 4

  println(dasar)
  println(diperluas)
bash
# menjalankan program koleksi-utuh.scala
scala-cli run koleksi-utuh.scala
text Keluaran
List(1, 2, 3)
List(1, 2, 3, 4)

Keluaran memperlihatkan dasar tetap berisi tiga elemen setelah diperluas dibuat karena operasi penambahan tidak mengubah koleksinya melainkan mengalokasikan koleksi baru. Koleksi yang tidak pernah berubah aman dibagikan ke banyak bagian program tanpa risiko isinya berubah tanpa disengaja, dan sifat itu menopang fungsi murni yang menerima koleksi sebagai masukan. Kombinasi keduanya menjadikan penalaran substitusi tetap sah dari ujung ke ujung program.

Ringkasan#

  • Fungsi murni menentukan keluarannya hanya dari masukannya dan tidak menimbulkan efek samping, sehingga seluruh komunikasinya berlangsung melalui parameter dan nilai kembalian.
  • Transparansi referensial memungkinkan ekspresi diganti nilai kembaliannya, sehingga kebenaran program dapat dinilai dengan uji substitusi.
  • Fungsi yang membaca keadaan global atau waktu sistem menghasilkan keluaran yang bergantung pada riwayat pemanggilan dan tidak dapat disubstitusi.
  • Fungsi murni pada data immutable mudah diuji dan aman dijalankan secara konkuren karena tidak ada keadaan bersama yang berubah.

Referensi#