Pada bagian sebelumnya, kita telah membahas mengenai pengertian dan deklarasi fungsi, fungsi lokal, serta fungsi literal. Bagian ini akan membahas mengenai bagaimana memanfaatkan fungsi pada Scala secara efektif, dengan berbagai teknik pemrograman fungsional yang ada.

Catatan: Seperti biasa, kode yang terdapat pada tulisan ini dapat diakses di repository blog.

Metode Pemanggilan Fungsi

Sebelum melihat teknik-teknik yang ada, terlebih dahulu akan dijelaskan berbagai cara pemanggilan fungsi yang unik pada Scala. Pada bagian sebelumnya, kita telah menuliskan banyak fungsi, tetapi pemanggilan fungsi dilakukan dengan cara standar: ketika dipanggil fungsi harus memiliki jumlah parameter yang tetap, jumlah parameter yang tepat, dan urutan parameter juga harus sama.

def penjumlahanTigaBilangan(a: Int, b: Int, c: Int) = {
  println("Nilai a: " + a)
  println("Nilai b: " + b)
  println("Nilai c: " + c)
  println("Nilai a + b + c: " + (a + b + c))
}

// Output:
// Nilai a: 1
// Nilai b: 2
// Nilai c: 3
// Nilai a + b + c: 6
penjumlahanTigaBilangan(1, 2, 3)

Tetapi tentunya Scala memberikan fasilitas untuk memanggil fungsi tersebut dengan cara yang berbeda. Metode pemanggilan fungsi khusus ini diciptakan untuk memenuhi kebutuhan-kebutuhan khusus, yang banyak ditemukan karena peran sentral fungsi dalam Scala. Adapun metode pemanggilan khusus yang disediakan Scala adalah: parameter berulang, penamaan parameter, serta parameter standar.

Parameter Berulang

Terkadang sebuah fungsi memerlukan jumlah parameter yang fleksibel, sesuai dengan kemauan pengguna fungsi. Scala memungkinkan fungsi untuk memiliki jumlah parameter yang tidak tetap, selama parameter tersebut bertipe data sama dan merupakan parameter terakhir. Untuk membuat sebuah parameter berulang, tambahkan simbol * setelah tipe data parameter, seperti:

scala> def cetak(strs: String*) = {
           for(str <- strs)
             println(str)
       }
cetak: (strs: String*)Unit

Penambahan * pada akhir tipe data memungkinkan kita untuk memanggil fungsi dengan banyak parameter:

scala> cetak("Jumlah", "Parameter", "Tidak", "Penting")
Jumlah
Parameter
Tidak
Penting

Jika dilihat dari dalam fungsi, tipe data dari parameter berulang adalah Array. Ketika mendefinisikan parameter sebagai String*, maka kita dapat memperlakukan parameter tersebut selayaknya sebuah Array[String]. Sayangnya, kita tidak dapat secara otomatis mengirimkan Array kepada parameter tersebut, seperti berikut:

scala> val arr = Array("Ini", "Array", "String")
arr: Array[java.lang.String] = Array(Ini, Array, String)

scala> cetak(arr)
<console>:10: error: type mismatch;
 found   : Array[java.lang.String]
 required: String
              cetak(arr)
                    ^

Hal ini dikarenakan oleh tipe data yang dihasilkan adalah tidak benar-benar Array, melainkan WrappedArray. Jalankan kode berikut:

scala> def tipeArgumen(args: String*) = println(args.getClass.getName)
tipeArgumen: (args: String*)Unit

scala> tipeArgumen("testing", "check", "satu", "dua")
scala.collection.mutable.WrappedArray$ofRef

Maka akan terlihat jelas bahwa tipe data dari args bukan Array[String], melainkan WrappedArrayofRef. Kedua tipe data ini tentunya memiliki operasi yang sama, sesuai dengan operasi umum yang dimiliki oleh semua class koleksi Scala.

Jika tetap ingin mengirimkan parameter berupa Array, kita dapat memanggil fungsi tersebut dengan variabel yang diikuti oleh : _*, seperti berikut:

scala> cetak(arr: _*)
Ini
Array
String

: _* merupakan cara khusus mengatakan ke compiler untuk mengirimkan isi dari arr sebagai parameter ke cetak. Penjelasan mengenai bagiamana : _* bekerja di belakang layar, serta kenapa parameter berulang memiliki tipe data WrappedArrayofRef akan dilakukan ketika pembahasan class koleksi pada Scala.

Penamaan Argumen

Pemanggilan fungsi biasanya mengharuskan parameter fungsi dikirimkan secara terurut (seperti yang ditunjukkan pad awal tulisan). Misalkan fungsi untuk menghitung tekanan dalam zat cair berikut:

scala> def tekanan(rho: Double, g: Double, h: Double) = {
           rho * g * h
       }
tekanan: (rho: Double, g: Double, h: Double)Double

scala> tekanan(1000, 9.8, 10)
res7: Double = 98000.0

rho, g, dan h akan dibaca secara terurut (dalam contoh di atas, rho = 1000, g = 9.8, dan h = 10). Tetapi terkadang kita ingin memasukkan parameter secara acak, misalnya ketika kita ingat nama parameter tetapi tidak ingat urutannya. Hal tersebut dapat dilakukan dengan menambahkan nama parameter sebelum pengisian nilai, seperti berikut:

 scala> tekanan(g = 9.8, h = 100, rho = 350)
 res0: Double = 343000.00000000006

(secara tidak sengaja saya menunjukkan kenapa Double sangat berbahaya jika dipakai untuk menghitung uang. Oops, jadi ngelantur)

Kembali ke topik, pemanggilan menggunakan nama parameter juga dapat digabungkan dengan pemanggilan berdasarkan urutan, contohnya:

scala> tekanan(1000, h = 10, g = 9.8)
res1: Double = 98000.0

Tetapi perhatikan bahwa setelah kita mulai memanggil berdasarkan nama, compiler tidak akan mengizinkan pemanggilan berdasarkan urutan lagi (karena tidak terdapat cara yang baik untuk menebak dengan benar):

scala> tekanan(1000, h = 10, 9.8)
<console>:9: error: positional after named argument.
              tekanan(1000, h = 10, 9.8)
                                    ^

Penamaan parameter seringkali digunakan bersamaan dengan parameter standar, yang akan dijelaskan pada bagian selanjutnya.

Parameter Standar

Scala memungkinkan kita untuk memberikan nilai standar pada parameter fungsi. Jika kita tidak mengisikan nilai parameter tersebut, maka secara otomatis nilai akan diambil dari nilai standar. Misalkan kode berikut:

scala> def bagi(pembilang: Double, penyebut: Double = 1) = pembilang / penyebut
bagi: (pembilang: Double, penyebut: Double)Double

akan memungkinkan kita untuk memanggil fungsi dengan atau tanpa penyebut:

scala> bagi(10, 2)
res0: Double = 5.0

scala> bagi(10)
res1: Double = 10.0

dan ketika kedua parameter memiliki nilai standar, maka kita dapat memanggil fungsi seolah-olah fungsi tersebut tidak memiliki parameter:

scala> def bagi(pembilang: Double = 1, penyebut: Double = 1) = pembilang / penyebut
bagi: (pembilang: Double, penyebut: Double)Double

scala> bagi()
res3: Double = 1.0

Penamaan parameter juga menjadi berguna, misalnya jika kita hanya ingin mengubah nilai dari penyebut ataupun pembilang saja:

scala> bagi(penyebut = 2)
res12: Double = 0.5

scala> bagi(pembilang = 3)
res13: Double = 3.0

Placeholder Syntax

Pada saat menuliskan fungsi literal, kerap kali kita diharuskan untuk menulis kode yang berulang. Perhatikan kode berikut:

scala> val kumpulan = List(-20, -9, -3, 0, 2, 18, 29)
kumpulan: List[Int] = List(-20, -9, -3, 0, 2, 18, 29)

scala> kumpulan.filter(x => x > 10)
res0: List[Int] = List(18, 29)

Penulisan fungsi literal x => x > 10 sangat tidak optimal, karena:

  1. x yang adalah elemen tunggal dari kumpulan harus dituliskan dua kali, padahal hanya digunakan sekali.
  2. Dalam fungsi literal ini, pada dasarnya yang dibutuhkan hanyalah ekspresi x > 10, karena ekspresi tersebutlah yang akan digunakan filter untuk menentukan apakah satu elemen harus disaring atau tidak.

Karena pada dasarnya ekspresi yang penting adalah x > 10, bukankah akan sangat menyenangkan jika kita dapat langsung menuliskan bagian tersebut saja? Scala memberikan fasilitas ini melalui placeholder syntax. Notasi yang digunakan ialah _:

scala> kumpulan.filter(_ > 10)
res1: List[Int] = List(18, 29)

Simbol _ dapat diibaratkan sebagai tanda pengganti (makanya namanya adalah placeholder), yang mana ketika fungsi dijalankan maka _ akan digantikan oleh elemen dari kumpulan. _ akan menjadi -20 pada saat filter mengecek elemen pertama, menjadi -9 ketika filter mengecek elemen kedua, dan seterusnya.

Placeholder juga dapat digunakan pada lebih dari satu argumen, selama parameter tersebut hanya digunakan satu kali saja. Misalnya, fungsi sortWith pada class kumpulan Scala menerima dua argumen sebagai dasar perbandingan untuk melakukan pengurutan. Kita dapat menggunakan dua placeholder ketika memanggil sortWith:

scala> kumpulan.sortWith(_ > _)
res0: List[Int] = List(29, 18, 2, 0, -3, -9, -20)

Yang pada dasarnya adalah sama dengan:

scala> kumpulan.sortWith((x, y) => x > y)
res1: List[Int] = List(29, 18, 2, 0, -3, -9, -20)

Lagi-lagi, compiler secara otomatis menebak tipe data x dan y dengan benar, sesuai dengan tipe data elemen kumpulan. Karena kumpulan bertipe List[Int], maka x dan y pastilah adalah Int. Sayangnya, compiler tidak selalu dapat menebak tipe data dengan benar. Misalnya, kode berikut menghasilkan error:

scala> val fungsi = _ + _
<console>:7: error: missing parameter type for expanded function ((x$1, x$2) => x$1.$plus(x$2))
       val fungsi = _ + _
                       ^

Karena compiler tidak memiliki cukup informasi untuk mengetahui tipe data dari kedua _ yang diberikan. Untuk memperbaiki kesalahan tersebut, kita dapat memberikan tipe data ke _:

scala> val fungsi = (_: Int) + (_: Int)
fungsi: (Int, Int) => Int = <function2>

sehingga fungsi dapat dipanggil seperti berikut:

scala> fungsi(10, 15)
res2: Int = 25

Perhatikan bahwa tipe data dari fungsi adalah (Int, Int) => Int = <function2>, yaitu fungsi yang memiliki dua parameter. Hal ini menunjukkan bahwa dua _ yang kita berikan masing-masing diubah menjadi argumen yang berbeda, bukan satu argumen yang digunakan berulang kali.

Kegunaan lainnya dari placeholder yaitu untuk menggantikan keseluruhan parameter fungsi. Misalkan untuk fungsi println yang hanya memerlukan satu parameter, kita dapat menuliskan:

scala> kumpulan.foreach(println _)
-20
-9
3
0
2
18
29

yang akan dianggap sama dengan:

scala> kumpulan.foreach(x => println(x))
-20
-9
3
0
2
18
29

Tetapi ingat, bahwa pemanggilan placeholder seperti ini mengharuskan karakter spasi (" ") setelah nama fungsi. Hal ini dikarenakan karakter _ adalah simbol nama fungsi yang valid, sehigga jika tidak menggunakan spasi compiler akan menganggap nama fungsi anda adalah nama yang diakhiri oleh _, seperti berikut:

scala> kumpulan.foreach(println_)
<console>:9: error: not found: value println_
              kumpulan.foreach(println_)
                               ^

Ketika kita menggunakan placeholder syntax untuk menggantikan keseluruhan parameter seperti di atas, fungsi yang dipanggil adalah aplikasi fungsi parsial. Apa itu aplikasi fungsi parsial?

Aplikasi Fungsi Parsial (Partially Applied Function)

Fungsi parsial adalah fungsi yang hanya diaplikasikan sebagian. Aplikasi fungsi maksudnya menerepkan parameter-parameter fungsi ke dalam fungsi tersebut. Misalnya jika kita memiliki fungsi total sebagai berikut:

scala> def total(a: Int, b: Int, c: Int) = a + b + c
total: (a: Int, b: Int, c: Int)Int

Maka dapat dikatakan bahwa kita memiliki fungsi, bernama total, yang belum diaplikasikan. Pengaplikasian fungsi baru dilakuka ketika kita memanggil fungsi tersebut sebagai berikut:

scala> total(10, 20, 30)
res0: Int = 60

Pada kode di atas, fungsi total diaplikasikan terhadap parameter 10, 20, dan 30. Karena fungsi memiliki tiga buah parameter dan kita mengaplikasikan seluruh parameter tersebut ke dalam fungsi, maka kita melakukan aplikasi penuh terhadap fungsi tersebut. Aplikasi fungsi parsial baru akan terjadi ketika kita mengaplikasikan hanya sebagian parameter, atau kita tidak mengaplikasikan parameter fungsi sama sekali. Misalkan:

 scala> val totalTambah10 = total(10, _: Int, _: Int)
 totalTambah10: (Int, Int) => Int = <function2>

Dapat dilihat bagaimana pada fungsi totalTambah10 di atas kita hanya mengaplikasikan parameter pertama dari total. Hal ini mengakibatkan dua hal:

  1. Pemanggilan fungsi totalTambah10 menjadi hanya memerlukan dua parameter, karena parameter pertama telah diaplikasikan.
  2. Karena mengaplikasikan fungsi total, totalTambah10 masih tetap akan menjumlahkan kedua parameternya, hanya saja total dari kedua parameter tersebut akan ditambahkan lagi ke 10, yang sudah diaplikasikan pada saat deklarasi totalTambah10.

Jika kita melakukan pemanggilan terhadap fungsi di atas maka kita akan mendapatkan hasil seperti berikut:

 scala> totalTambah10(15, 20)
 res0: Int = 45

Adapun langkah-langkah aplikasi fungsi tersebut adalah sebagai berikut:

val totalTambah10 = total(10, _: Int, _: Int)

// fungsi di atas dapat dianggap sama dengan:
totalTambah10 = total(10, _: Int, _: Int)

// yang jika diubah lagi akan menjadi:
totalTambah10 = total(10, b: Int, c: Int)

// dan kemudian:
totalTambah10 = 10 + b + c

// sehingga totalTambah10(15, 20) menjadi:
totalTambah10(15, 20) = 10 + 15 + 20 = 45

Catatan: Potongan kode di atas tidak dapat dijalankan pada Scala, karena bukan merupakan kode yang valid. Potongan kode hanya dibuat untuk mempermudah pengertian.

Aplikasi fungsi parsial juga tidak membatasi jumlah dan urutan parameter yang harus diaplikasikan, sehingga aplikasi dua buah parameter pada fungsi total seperti berikut:

scala> val totalSatu = total(10, _: Int, 5)
totalSatu: Int => Int = <function1>

scala> totalSatu(10)
res1: Int = 25

dapat dilakukan. Kita bahkan dapat mengaplikasikan 0 parameter:

scala> val totalKW = total(_: Int, _:Int, _:Int)
totalKW: (Int, Int, Int) => Int = <function3>

scala> totalKW(1, 2, 3)
res2: Int = 6

atau lebih mudahnya menggunakan sintaks singkat seperti pada println sebelumnya:

scala> val totalKW2 = total _
totalKW2: (Int, Int, Int) => Int = <function3>

scala> totalKW2(2, 3, 4)
res3: Int = 9

Perlu diperhatikan bahwa simbol _ harus digunakan jika ingin tidak mengaplikasikan seluruh argumen, dan harus terdapat karakter spasi (" ") di antara nama fungsi dan _.

scala> val tot = total
<console>:8: error: missing arguments for method total in object $iw;
follow this method with `_' if you want to treat it as a partially applied function
       val tot = total
                 ^

scala> val tot = total_
<console>:7: error: not found: value total_
       val tot = total_
                 ^

Fungsi Literal (Lanjutan)

Pada tulisan sebelumnya, kita telah membahas mengenai fungsi literal. Bagian ini akan melanjutkan pembahasan tersebut, untuk melihat berbagai teknik tambahan yang dapat digunakan pada fungsi literal. Jika anda belum membaca tulisan sebelumnya, sekarang adalah saat yang tepat, karena tulisan ini tidak dapat dibaca tanpa mengerti isi tulisan sebelumnya.

Kita telah melihat bagaimana fungsi literal dapat digunakan selayaknya variabel, sehingga dapat dimasukkan ke dalam variabel:

scala> var cekPositif = (b: Int) => if (b > 0) true else false
cekPositif: Int => Boolean = <function1>

scala> cekPositif(10)
res0: Boolean = true

scala> cekPositif(-10)
res1: Boolean = false

tetapi ternyata kemampuan fungsi literal tidak hanya sampai di sana. Fungsi literal dapat juga digunakan sebagai parameter dari fungsi lainnya. Untuk mempermudah pengertian, kita akan mencoba menyelesaikan sebuah permasalahan memanfaatkan fungsi literal.

Misalkan jika kita ingin menghitung total jumlah deret bilangan dari x ke y seperti berikut (akan lebih mudah dimengerti jika anda langsung membaca bagian implementasi :p):

// deklarasi
hitungDeret(x, y) = x + (x + 1) + (x + 2) + ... + (y - 1) + y
di mana: a < b. Jika a > b maka hasil = 0

// implementasi
hitungDeret(1, 3) = 1 + 2 + 3         = 6
hitungDeret(2, 6) = 2 + 3 + 4 + 5 + 6 = 20
hitungDeret(8, 1)                     = 0 

maka kita dapat menuliskan fungsi tersebut ke dalam Scala dengan kode berikut:

scala> def hitungDeret(x: Int, y: Int): Int = {
         if (x > y) 0 else x + hitungDeret(x + 1, y)
       }
hitungDeret: (x: Int, y: Int)Int

scala> hitungDeret(1, 3)
res2: Int = 6

scala> hitungDeret(2, 6)
res3: Int = 20

scala> hitungDeret(8, 1)
res4: Int = 0

Cara kerja dari fungsi hitungDeret adalah:

  1. Jika nilai x lebih besar dari y, kembalikan 0.
  2. Jika tidak, tambahkan x dengan hasil kalkulasi dari fungsi hitungDeret yang nilai x-nya telah dinaikkan satu.

Untuk mempermudah pengertian, berikut adalah langkah-langkah yang dijalankan fungsi ketika kita memanggil hitungDeret(2, 6):

 0: hitungDeret(2, 6)
 1: 2 + hitungDeret(3, 6)
 2: 2 + 3 + hitungDeret(4, 6)
 3: 2 + 3 + 4 + hitungDeret(5, 6)
 4: 2 + 3 + 4 + 5 + hitungDeret(6, 6)
 5: 2 + 3 + 4 + 5 + 6 + hitungDeret(7, 6)
 6: 2 + 3 + 4 + 5 + 6 + 0
 7: 2 + 3 + 4 + 5 + 6
 8: 2 + 3 + 4 + 11
 9: 2 + 3 + 15
10: 2 + 18
11: 20

Pada langkah 1 sampai 5, fungsi melakukan pemanggilan hitungDeret terus menerus, dengan penambahan 1 terhadap parameter x setiap kalinya. Pemanggilan baru berhenti ketika nilai x lebih besar dari y, di mana fungsi akan mengembalikan 0 alih-alih fungsi hitungDeret seperti pada langkah-langkah sebelumnya. Setelah tidak terdapat kembalian fungsi lagi, Scala akan melakukan perhitungan terhadap nilai kembalian, dari fungsi yang terakhir (langkah 6) sampai didapatkan hasil perhitungan (langkah 11).

Sekarang bayangkan jika kita ingin menghitung nilai penjumlahan dari kuadrat deret bilangan, sehingga:

hitungKuadratDeret(1, 3) = 1^2 + 2^2 + 3^2             = 14
hitungKuadratDeret(2, 6) = 2^2 + 3^2 + 4^2 + 5^2 + 6^2 = 90
hitungKuadratDeret(8, 1) = 0

maka kita dapat menggunakan fungsi yang sangat mirip dengan hitungDeret:

scala> def hitungKuadratDeret(x: Int, y: Int): Int = {
         if (x > y) 0 else x*x + hitungKuadratDeret(x + 1, y)
       }
hitungKuadratDeret: (x: Int, y: Int)Int

scala> hitungKuadratDeret(1, 3)
res0: Int = 14

scala> hitungKuadratDeret(2, 6)
res1: Int = 90

scala> hitungKuadratDeret(8, 1)
res2: Int = 0

Perhatikan bagaimana perbedaan pada hitungDeret dan hitungKuadratDeret hanyalah pada bagian penambahan, di mana hitungDeret melakukan penambahan terhadap x, sementara hitungKuadratDeret melakukan penambahan terhadap x*x!

Perbedaan hanya pada bagian yang ditandai

Karena perbedaan yang sangat kecil ini, kita mungkin dapat melakukan abstraksi terhadap bagian yang diwarnai dengan mengubahnya ke dalam fungsi tersendiri, sehingga fungsi tersebut dapat ditentukan oleh pengguna:

scala> def hitungDeret(x: Int, y: Int, fungsiBilangan: Int => Int): Int = {
         if (x > y) 0 else fungsiBilangan(x) + hitungDeret(x + 1, y, fungsiBilangan)
       }
hitungDeret: (x: Int, y: Int, fungsiBilangan: Int => Int)Int

Ya, kita baru saja membuat fungsi yang menerima fungsi lain sebagai parameter. hitungDeret menerima fungsiBilangan yang bertipe Int => Int sebagai parameter. Int => Int berarti fungsi yang menerima satu bilangan Int dan mengembalikan bilangan bertipe Int. Kita lalu dapat memanggil fungsi tersebut untuk melakukan perhitungan deret maupun kuadrat deret:

scala> hitungDeret(1, 3, (x: Int) => x)
res0: Int = 6

scala> hitungDeret(1, 3, (x: Int) => x * x)
res1: Int = 14

atau lebih singkatnya:

scala> hitungDeret(1, 3, x => x)
res6: Int = 6

scala> hitungDeret(1, 3, x => x * x)
res7: Int = 14

Placeholder dapat digunakan di sini, misalnya jika kita ingin menghitung deret x + 1:

// Hasil: (1 + 1) + (2 + 1) + (3 + 1) = 9
scala> hitungDeret(1, 3, _ + 1)
res9: Int = 9

dan tentunya juga, aplikasi fungsi parsial:

scala> val hitungDeretKuadrat = hitungDeret(_: Int, _: Int, x => x * x)
hitungDeretKuadrat: (Int, Int) => Int = <function2>

scala> hitungDeretKuadrat(2, 6)
res0: Int = 90

Closure

Sampai saat ini, pengunaan variabel pada fungsi literal yang kita ciptakan hanya terbatas pada variabel yang terdapat di dalam fungsi tersebut. Misalkan pada fungsi berikut:

scala> val tambah = (x: Int) => x + 1
tambah: Int => Int = <function1>

hanya terdapat satu variabel yang digunakan pada badan fungsi (x + 1), yaitu x. Sebenarnya, kita juga dapat menggunakan variabel yang berada pada luar fungsi, seperti berikut:

scala> var angka = 1000
angka: Int = 1000

scala> val tambahAngka = (x: Int) => x + angka
tambahAngka: Int => Int = <function1>

Tentunya variabel angka harus telah dideklarasikan sebelumnya. Menggunakan variabel yang belum dideklarasikan akan menghasilkan error:

scala> val tambahAnggota = (x: Int) => x + anggota
<console>:7: error: not found: value anggota
       val tambahAnggota = (x: Int) => x + anggota
                                           ^

Kita dapat menggunakan fungsi tambahAngka di atas dengan bebas:

scala> tambahAngka(10)
res0: Int = 1010

scala> tambahAngka(536)
res1: Int = 1536

Dan bahkan ketika kita menggantikan nilai angka, tambahAngka akan mengetahui perubahan tersebut:

scala> angka = 200
angka: Int = 200

scala> tambahAngka(56)
res2: Int = 256

scala> tambahAngka(1)
res3: Int = 201

Variabel angka pada fungsi tambahAngka di atas dikenal dengan nama variabel bebas, karena makna / nilai dari angka tidak bergantung kepada tambahAngka. Variabel x dikenal sebagai variabel terikat, karena nilai x hanya berlaku di dalam fungsi literal. Nilai fungsi yang diciptakan dari fungsi literal jenis ini dikenal dengan nama closure.

Selain dapat digunakan dengan variabel bebas yang berada pada lingkup luar seperti pada contoh sebelumnya, closure juga dapat digunakan dengan variabel yang ada di dalam lingkup dalam fungsi lain, misalnya:

scala> def buatPenambahAngka(n: Int) = (x: Int) => x + n
buatPenambahAngka: (n: Int)Int => Int

dan dalam kasus ini, setiap nilai fungsi dari buatPenambahAngka akan memiliki n yang berbeda-beda.

scala> val tambah100 = buatPenambahAngka(100)
tambah100: Int => Int = <function1>

scala> tambah100(28)
res5: Int = 128

scala> val tambah10 = buatPenambahAngka(10)
tambah10: Int => Int = <function1>

scala> tambah10(54)
res6: Int = 64

Catatan: Perhatikan juga bahwa fungsi buatPenambahAngka pada dasarnya mengembalikan fungsi literal yang bertipe Int => Int. Jadi, selain dapat dikirimkan sebagai parameter, fungsi literal juga dapat dikembalikan dari sebuah fungsi! Sederhananya, segala hal yang dapat dilakukan kepada variabel dapat juga dilakukan kepada fungsi literal.

Meskipun konsep dari closure sangat sederhana, kita dapat menggunakan closure untuk mengembangkan abstraksi yang sangat baik. Sayangnya, contoh pengunaan abstraksi closure akan memerlukan konsep lainnya, yaitu function currying untuk dapat digunakan secara maksimal. Konsep function currying akan dijelaskan pada bagian selanjutnya.

Fungsi Curry (Function Currying)

Trivia: Kenapa disebut currying dan ketika diterjemahkan ke bahasa Indonesia tetap masih Curry? Karena penemu teknik ini bernama Haskell Curry. Bahasa Haskell juga dinamakan berdasarkan legenda matematika ini.

Currying merupakan proses untuk memecah satu fungsi yang memiliki banyak parameter menjadi banyak fungsi yang masing-masing memiliki satu parameter. Jika awalnya kita memiliki fungsi dengan lima parameter, setelah melakukan proses currying maka kita akan memiliki lima fungsi dengan satu parameter. Misalkan fungsi berikut:

scala> def bagi(x: Double, y: Double) = x / y
bagi: (x: Double, y: Double)Double

Jika diaplikasikan proses currying maka akan menjadi dua fungsi yang masing-masing memiliki satu parameter seperti berikut:

scala> def bagi(x: Int) = (y: Int) => x / y
bagi: (x: Int)Int => Int

Terdapat dua fungsi pada kode di atas, yaitu fungsi bagi yang menerima parameter x bertipe Double dan mengembalikan fungsi kedua, yaitu sebuah fungsi literal (y: Double) => x / y yang menerima sebuah parameter y bertipe Double dan mengembalikan nilai Double, yaitu x / y. Fungsi bagi yang baru ini tentunya tidak dapat dipanggil langsung dengan dua parameter, melakinkan harus dipanggil dua kali, memberikan masing-masing parameter ke fungsi yang berbeda:

scala> val ba = bagi(8)
ba: Double => Double = <function1>

scala> ba(4)
res16: Double = 2.0

Scala pastinya memberikan sintaks sederhana untuk currying, yaitu dengan memberikan dua (atau lebih) daftar parameter pada kode kita:

scala> def bagi(x: Double)(y: Double) = x / y
bagi: (x: Double)(y: Double)Double

scala> bagi(8)(2)
res19: Double = 4.0

scala> bagi(27)(3)
res20: Double = 9.0

scala> def total(x: Int)(y: Int)(z: Int) = x + y + z
total: (x: Int)(y: Int)(z: Int)Int

scala> total(10)(20)(10)
res21: Int = 40

scala> total(1)(2)(3)
res22: Int = 6

Kita dapat mengakses fungsi kembalian dari bagi menggunakan placeholder:

scala> val duaBagi = bagi(2)_
duaBagi: Double => Double = <function1>

sehingga kita dapat memanggil fungsi duaBagi lagi, misalnya untuk membagikan dua kepada delapan:

scala> duaBagi(8)
res24: Double = 0.25

Oke, Fungsi Curry ini keren. Lalu gunanya untuk apa dong? Kalau cuma memecah fungsi saja kan sangat tidak berguna, selain bisa pamer soal kemurnian matematis program kita?

Pertanyaan bagus. Kegunaan utama dari currying adalah untuk mengaplikasikan abstraksi. Misalnya, memanfaatkan currying kita dapat membuat struktur kontrol baru (if, for, dll). Bagaimana caranya?

Andaikan kita membangun sistem penjagaan gerbang masuk sebuah kastil, yang mengharuskan kita untuk memberikan kata kunci sebelum membuka gerbang. Jika kata kunci benar, maka kita akan mendapatkan sambutan, sementara jika kata kunci salah maka alaram akan aktif. Fungsi ini diaplikasikan ke seluruh gerbang yang ada dalam kastil, di mana tiap gerbang memiliki kata kunci yang sama tetapi memiliki sambutan berbeda. Fungsi jenis ini dapat dituliskan sebagai berikut:

scala> def withMagic(code: String, func: () => String) = {
         if (code == "alohomora") {
           println("Well hello, " + func())
         } else {
           println("Intruder alert! Intruder alert! Piertotum Locomotor! Protect your castle!")
         }
       }
withMagic: (code: String, func: () => String)Unit

dan dapat kita gunakan seperti berikut:

scala> withMagic("alohomora", () => "Harry Potter")
Well hello, Harry Potter

scala> withMagic("crucio", () => "Bellatrix")
Intruder alert! Intruder alert! Piertotum Locomotor! Protect your castle!

Catatan: Fungsi literal func tidak menerima parameter apapun dan mengembalikan String, karenanya fungsi ini tidak memiliki daftar parameter, yang dituliskan sebagai ().

Keunggulan dari kode seperti ini adalah pengecekan kata kunci tidak dilakukan secara manual oleh pengguna fungsi, sehingga kesalahan pengecekan tidak mungkin dilakukan, kecuali jika kesalahan terjadi pada fungsi withMagic. Adapun kekurangan fungsi ini adalah asteistik, di mana menuliskan () sangat tidak menyenangkan dan tidak enak dipandang.

Untungnya, Scala memberikan fasilitas untuk membuang kurung tersebut. Fasilitas ini bernama by-name parameter, yang memungkinkan kita untuk menuliskan nama fungsi tanpa parameter langsung jika fungsi memang tidak memiliki parameter. Pengunaan by-name parameter pada fungsi withMagic adalah sebagai berikut:

scala> def withMagic(code: String, func: => String) = {
         if (code == "alohomora") {
           println("Well hello, " + func)
         } else {
           println("Intruder alert! Intruder alert! Piertotum Locomotor! Protect your castle!")
         }
       }
withMagic: (code: String, func: => String)Unit

Perbedaannya adalah, kita mendefinisikan fungsi langsung tanpa kurung () jika tidak menggunakan parameter (func: => String), dan pemanggilan fungsi juga tidak memerlukan kurung lagi, seperti berikut:

scala> withMagic("alohomora", "Hermione Granger")
Well hello, Hermione Granger

scala> withMagic("impedimenta", "Lucius")
Intruder alert! Intruder alert! Piertotum Locomotor! Protect your castle!

dan jika kita menerapkan currying ke dalam fungsi ini maka fungsi akan menjadi:

scala> def withMagic(code: String)(func: => String) = {
         if (code == "alohomora") {
           println("Well hello, " + func)
         } else {
           println("Intruder alert! Intruder alert! Piertotum Locomotor! Protect your castle!")
         }
       }
withMagic: (code: String)(func: => String)Unit

scala> withMagic("alohomora")("Ronald")
Well hello, Ronald

scala> withMagic("imperio")("Tom")
Intruder alert! Intruder alert! Piertotum Locomotor! Protect your castle!

Tentunya fungsi ini masih dapat ditingkatkan keindahannya lebih jauh. Pada Scala, jika sebuah fungsi memiliki hanya satu parameter, kita dapat memanggil fungsi menggunakan {}, selain menggunakan () seperti biasa. Misalnya, fungsi println dapat dipanggil seperti berikut:

scala> println("Hello")
Hello

scala> println { "Hello" }
Hello

Dan karena withMagic memiliki dua fungsi yang masing-masing menerima satu parameter, kita dapat memanggil fungsi tersebut seperti:

scala> withMagic("alohomora") {
           "Neville"
       }
Well hello, Neville

Atau bahkan:

scala> withMagic("alohomora") {
           val name = "Neville "
           val lastName = "Longbottom"
           name + lastName
       }
Well hello, Neville Longbottom

karena pada dasarnya func yang adalah parameter fungsi kedua adalah sebuah fungsi yang dapat memiliki banyak perintah, selama ia mengembalikan String. Nah, jadi fungsi withMagic menjadi mirip dengan apa?

for(val i = 1 to 10) {
  // ...
}

if(x > y) {
  // ...
}

Oh. Kita bahkan dapat mengimplementasikan for seperti yang ada pada bahasa lainnya jika diinginkan:

scala> def FOR(start: Unit, condition: => Boolean, inc: => Unit)(body: => Unit): Unit = {
         if (condition) {
           body
           inc
           FOR(0, condition, inc)(body)
         }
       }
FOR: (start: Unit, condition: => Boolean, inc: => Unit)(body: => Unit)Unit

scala> var i = 0
i: Int = 0

scala> FOR(i = 0, i <= 10, i += 1) {
         println(i)
       }
0
1
2
3
4
5
6
7
8
9
10

Meskipun sangat tidak disaranakn untuk dilakukan, karena model perintah for seperti itu yang sangat tidak aman, tidak fungsional, dan memiliki terlalu banyak fungsi yang tidak mengembalikan nilai.

Catatan: Kode untuk fungsi FOR di atas diambil dari jawaban pada StackOverflow di sini.

Kesimpulan

Artikel ini telah menjelaskan berbagai teknik pemrograman fungsional yang bekerja dengan fungsi untuk melakukan abstraksi kode. Kita juga melihat berbagai fitur Scala yang berpotensi untuk digunakan dalam berbagai sekenario, untuk mempermudah pemrograman dan memperkaya bahasa. Tulisan selanjutnya akan memberikan contoh mengenai pengunaan berbagai teknik ini di dalam kode program secara praktis.

Akhir kata, happy coding!

comments powered by Disqus

Daftar Isi