Fungsi sebagai nilai

Lambda, closure, dan fungsi tingkat tinggi sebagai fondasi gaya fungsional.

Daftar isi

Scala memperlakukan fungsi sebagai nilai sebagaimana angka dan string, sehingga fungsi dapat disimpan dalam val, dikirim sebagai argumen, dan dikembalikan dari fungsi lain. Topik ini membahas lambda, tipe fungsi A => B, fungsi tingkat tinggi, serta closure yang menangkap lingkup pembuatannya. Mekanisme-mekanisme ini menjadi fondasi gaya fungsional, dan map beserta filter yang telah dibahas pada modul koleksi merupakan penggunaannya yang paling sering ditemui. Pembaca yang telah menyelesaikan topik ini dapat membaca pipeline koleksi sebagai pemanggilan fungsi biasa, bukan sebagai sintaks khusus yang harus dihafal.

Lambda dan tipe fungsi#

Lambda adalah ungkapan yang membuat nilai fungsi tanpa memberinya nama, ditulis dengan bentuk parameter => hasil. Setiap nilai fungsi memiliki tipe fungsi yang menyerupai A => B, yaitu tipe yang menyatakan masukan bertipe A dan keluaran bertipe B. Tipe tersebut dituliskan secara eksplisit pada val agar pengetikan nilainya terkunci, sedangkan parameter lambda tidak perlu diberi anotasi karena tipenya sudah diketahui dari konteks. Nilai fungsi dipanggil dengan menuliskan argumennya di dalam tanda kurung setelah namanya, persis seperti pemanggilan metode.

Program berikut menyimpan dua lambda dalam val, lalu memanggil keduanya satu kali.

scala fungsi-nilai.scala
// Lambda disimpan sebagai nilai dalam val
@main def fungsiNilai(): Unit =
  val kuadrat: Int => Int = x => x * x
  val salam: String => String = nama => s"Halo, $nama"

  println(kuadrat(9))
  println(salam("Dewi"))
bash
# menjalankan program fungsi-nilai.scala
scala-cli run fungsi-nilai.scala
text Keluaran
81
Halo, Dewi

Keluaran memperlihatkan kedua nilai fungsi bekerja dengan tipe yang berbeda. kuadrat bertipe Int => Int dan menghasilkan 81 dari masukan 9, sedangkan salam bertipe String => String dan membentuk teksnya melalui interpolasi string. Apabila logikanya membutuhkan lebih dari satu langkah, tubuh lambda dapat ditulis sebagai blok setelah =>, dan nilai ekspresi terakhir pada blok itu menjadi keluarannya.

Fungsi tingkat tinggi#

Fungsi tingkat tinggi (higher-order function) adalah fungsi yang memiliki parameter berupa fungsi, mengembalikan fungsi, atau keduanya. Kemampuan ini muncul langsung dari sifat fungsi sebagai nilai, karena apa pun yang dapat disimpan dalam variabel juga dapat dikirim dan dikembalikan. Parameter berupa fungsi ditulis dengan tipe fungsi yang sama, misalnya f: Int => Int, sehingga perilaku yang diinginkan pemanggil dapat disisipkan melalui argumen. map dan filter pada modul koleksi adalah fungsi tingkat tinggi, dan topik ini membahas mekanisme yang membuat keduanya mungkin.

Program berikut memuat fungsi terapkanSemua yang menerima daftar beserta fungsi transformasinya, serta fungsi pengaliDengan yang mengembalikan fungsi pengali baru setiap kali dipanggil.

scala penerapan.scala
// Fungsi tingkat tinggi: menerima fungsi dan menghasilkan fungsi
@main def penerapan(): Unit =
  val nilai: List[Int] = List(12, 7, 20, 5)

  def terapkanSemua(daftar: List[Int], f: Int => Int): List[Int] =
    daftar.map(f)

  def pengaliDengan(faktor: Int): Int => Int =
    n => n * faktor

  val duaKali: Int => Int = pengaliDengan(2)
  val hasil: List[Int] = terapkanSemua(nilai, duaKali)

  println(hasil)
bash
# menjalankan program penerapan.scala
scala-cli run penerapan.scala
text Keluaran
List(24, 14, 40, 10)

Setiap angka pada keluaran adalah masukan yang dikalikan dua oleh duaKali, yaitu fungsi yang dihasilkan pengaliDengan(2). Fungsi terapkanSemua sendiri tidak mengetahui perilaku transformasinya, karena perilaku itu dibawa oleh parameternya. Pola seperti ini membuat satu fungsi dapat digunakan ulang untuk banyak kebutuhan, cukup dengan mengganti fungsi yang dikirim.

Scala juga mengizinkan metode yang sudah ada digunakan sebagai nilai fungsi. Apabila sebuah metode ditulis pada posisi yang menuntut tipe fungsi, compiler mengubah metode itu menjadi nilai fungsi, dan proses ini disebut eta-expansion.

scala Contoh: metode menjadi nilai fungsi
def panjang(s: String): Int = s.length

val f: String => Int = panjang  // eta-expansion terjadi di sini

Closure yang menangkap lingkupnya#

Closure (closure) adalah nilai fungsi yang menyimpan rujukan ke variabel-variabel pada lingkup tempat fungsi itu dibuat. Rujukan tersebut tetap hidup meskipun fungsi luarnya telah selesai dijalankan, sehingga tubuh lambda dapat membaca variabel yang bukan parameternya. Variabel yang ditangkap diikat pada saat fungsi dibuat, bukan pada saat fungsi dipanggil, dan setiap pembuatan fungsi menghasilkan pengikatan yang berdiri sendiri.

Program berikut menjadikan pembuatPengali sebagai pabrik closure. Setiap pemanggilannya membuat lambda yang menangkap nilai faktor pada pemanggilan itu.

scala pengali.scala
// Setiap pemanggilan menghasilkan closure dengan faktor tangkapannya sendiri
@main def pengaliTersimpan(): Unit =
  def pembuatPengali(faktor: Int): Int => Int =
    (n: Int) => n * faktor

  val tigaKali: Int => Int = pembuatPengali(3)
  val limaKali: Int => Int = pembuatPengali(5)

  println(tigaKali(4))
  println(limaKali(4))
bash
# menjalankan program pengali.scala
scala-cli run pengali.scala
text Keluaran
12
20

Keluaran memperlihatkan dua fungsi yang membawa faktor berbeda. tigaKali menangkap 3 sehingga masukan 4 menghasilkan 12, sedangkan limaKali menangkap 5 sehingga masukan yang sama menghasilkan 20. Kedua closure dibuat dari tubuh lambda yang sama, tetapi masing-masing menyimpan lingkup tangkapannya sendiri, sehingga perubahan pada satu fungsi tidak pernah memengaruhi fungsi yang lain.

Kesalahan umum#

Kesalahan yang paling sering muncul ketika fungsi mulai diperlakukan sebagai nilai adalah mencampur dua konsep yang berbeda, yaitu memanggil fungsi dan menyebut fungsinya. Ekspresi tigaKali(4) menghasilkan Int karena fungsi dipanggil, sedangkan ekspresi tigaKali tanpa tanda kurung adalah nilai fungsi bertipe Int => Int. Menyimpan hasil pemanggilan dan menyimpan fungsinya sendiri menghasilkan dua hal yang sangat berbeda, dan perbedaan ini baru terasa ketika fungsi hendak diteruskan ke fungsi tingkat tinggi lain.

Kesalahan kedua adalah menganggap tipe hasil selalu ditentukan oleh compiler dengan benar pada setiap situasi. Ketika lambda ditulis tanpa anotasi dan diteruskan langsung ke fungsi tingkat tinggi, compiler menurunkan tipenya dari konteks pemanggilan. Sebaliknya, apabila lambda diikat ke val tanpa anotasi tipe, compiler menuntut informasi tambahan dan menolak mengompilasi dengan pesan bahwa tipe tidak dapat ditebak. Perbaikannya sederhana, yaitu menuliskan anotasi tipe pada pengikatan, misalnya val panjang: String => Int = s => s.length, sehingga maksud program terbaca sekaligus tipe terjaga.

Ringkasan#

  • Lambda x => hasil membuat nilai fungsi tanpa nama, dan tipe fungsinya ditulis A => B untuk masukan bertipe A dengan keluaran bertipe B.
  • Fungsi tingkat tinggi menerima fungsi sebagai parameter, mengembalikan fungsi, atau keduanya, sehingga perilaku program dapat disisipkan melalui argumen.
  • Eta-expansion mengubah metode menjadi nilai fungsi ketika tipe fungsi diharapkan, dan Scala 3 melakukannya secara otomatis.
  • Closure menyimpan lingkup pembuatnya, sehingga setiap fungsi yang dihasilkan pabrik fungsi membawa nilai tangkapannya sendiri.

Referensi#