Fungsi merupakan komponen inti dari pemrograman fungsional pada umumnya, dan Scala pada khususnya. Tanpa pengetahuan yang mendalam mengenai fungsi, pengunaan Scala tidak akan efektif. Karenanya, sebelum membahas mengenai fitur-fitur lain pada Scala, kita terlebih dahulu akan masuk secara mendalam mengenai fungsi. Tulisan ini terdiri dari dua bagian, untuk menjaga panjang artikel.

Catatan: Kode yang digunakan pada program ini dapat diambil di repository kode blog.

Apa itu Fungsi?

Dalam ilmu komputer (dan pemorgraman pada umumnya), fungsi adalah sebuah komponen dari kode program yang berguna untuk menjalankan sekumpulan perintah tertentu. Fungsi lalu dapat dipanggil untuk dijalankan pada bagian manapun dari kode. Fungsi dapat disamakan dengan sebuah program kecil di dalam sebuah program, karena pada dasarnya fungsi melakukan hal yang sama dengan program: menerima nilai masukan tertentu, kemudian memberikan keluaran yang didapat dari hasil mengaplikasikan langkah-langkah tertentu. Karenanya, fungsi sering disebut dengan nama “subroutine” atau “subprogram”.

Gambar Ilustrasi Fungsi. Sumber: Wikipedia

Seperti yang dapat dilihat pada gambar, fungsi menerima masukan nilai tertentu, melakukan proses terhadap nilai tersebut, dan memberikan keluaran sesuai dengan hasil proses. Terdapat dua jenis fungsi dari sisi pemrograman, yaitu:

  1. Fungsi Murni (Pure Function), yang adalah fungsi yang tidak memberikan efek terhadap elemen di luar fungsi tersebut, dan akan selalu menghasilkan nilai keluaran yang sama jika diberikan masukan yang sama. Contoh fungsi: perhitungan sinus, cosinus, dan tangen.
  2. Fungsi Tidak Murni (Impure Function) merupakan antitesis dari fungsi murni. Dapat memberikan efek terhadap elemen-elemen di luar fungsi, dan tidak selalu memberikan hasil yang sama meskipun masukan terhadap fungsi sama. Contoh fungsi: fungsi IO (mis: println), bilangan acak.

Catatan: “Efek terhadap elemen di luar fungsi” dikenal juga sebagai side effect, yang pernah dibahas sekilas pada tulisan sebelumnya. Untuk pembahasan lengkap, artikel wikipedia berikut memberikan penjelasan yang sangat lengkap dan jelas.

Fungsi dapat dipanggil oleh kode lain jika diperlukan. Fungsi bahkan dapat memanggil dirinya sendiri. Fungsi yang memanggil dirinya sendiri dikenal dengan nama fungsi rekursif. Pembahasan mengenai fungsi rekursif hanya akan diberikan sekilas, karena dalamnya konsep rekursif. Detil mengenai fungsi rekursif akan dibahas pada artikelnya tersendiri.

Pada paradigma pemrograman fungsional, seorang penulis progam idealnya berusaha untuk menuliskan fungsi murni tanpa pengecualian. Bahasa pemrograman fungsional murni seperti Haskell biasanya bahkan tidak memungkinkan kita untuk menulis fungsi tidak murni. Karenanya, tulisan ini akan berfokus pada fungsi murni dan berbagai konsep-konsep yang berhubungan dengannya. Penjelasan mengenai konsep fungsi tentunya dibatasi pada konsep-konsep yang didukung oleh bahasa pemrograman Scala (seperti yang ada pada judul tulisan).

Deklarasi Fungsi

Scala memiliki dua cara penulisan fungsi, yaitu cara penulisan panjang dan pendek. Penulisan fungsi panjang dilakukan seperti berikut:

scala> def terkecil(x: Int, y: Int): Int = {
         if (x > y)
           y
         else
           x
       }
terkecil: (x: Int, y: Int)Int

Deklarasi fungsi dimulai dengan kata kunci def. def kemudiaan diikuti dengan nama fungsi (dalam contoh ini, terkecil) dan kemudian diikuti lagi oleh parameter-parameter yang digunakan oleh fungsi tersebut. Parameter-parameter ini disatukan dalam kurung (()) tepat setelah nama fungsi, dan dideklarasikan dalam format deklarasi variabel (NamaVariabel: TipeData). Deklarasi fungsi kemudian dilanjutkan dengan penambahan tipe data fungsi, yang diawali dengan titik dua (:). Tipe data fungsi dikenal dengan nama result type dalam dunia Scala (pada Java, hal ini dikenal dengan nama return type). Tipe data fungsi juga bersifat opsional pada Scala. Pada kebanyakan kasus, compiler akan sanggup menebak tipe data fungsi dengan benar (tetapi tidak semua kasus. MIsalnya, fungsi Rekursif mengharuskan penulisan result type). Begitupun, penulisan result type sangat disarankan, karena akan mempermudah perawatan kode.

Catatan: Jika dilihat dari satu sisi, result type dari fungsi dapat dibaca selayaknya deklarasi tipe variabel, seperti:

NamaFungsi: TipeFungsi

di mana NamaFungsi merupakan deklarasi lengkap fungsi (def kecil(...))

Elemen selanjutnya pada deklarasi fungsi ialah isi (badan) dari fungsi itu sendiri. Badan dari fungsi dituliskan di dalam kurung kurawal ({}), yang dipisahkan oleh tanda sama dengan (=) dari result type fungsi. Untuk sederhananya, gambar berikut menunjukkan aturan deklarasi fungsi Scala:

Deklarasi Fungsi Scala

Fungsi yang telah dideklarasikan tersebut kemudian dapat dipanggil dengan cara berikut:

scala> terkecil(10, 6)
res0: Int = 6

Sekarang mari kita lihat bagaimana fungsi ini bekerja. Di dalam badan fungsi, kita hanya memiliki satu ekspresi (bukan statement):

if (x > y)
  y
else
  x

Ya, kode di atas hanya terdiri dari satu ekspresi. Eksprsi ini bahkan menghasilkan sebuah nilai, yaitu y jika x > y dan x jika tidak. Perintah if dalam Scala menghasilkan nilai agar dapat digunakan seperti operator ternary dalam bahasa-bahasa lain. Misalnya, kode di atas dapat dituliskan menjadi hanya satu baris, seperti berikut:

if (x > y) y else x

dan kode tersebut sama dengan kode ini:

(x > y) ? y : x

pada Java. Dengan berdasarkan pengetahuan tersebut, maka fungsi terkecil yang dituliskan sebelumnya dapat ditulis ulang dengan cara singkat seperti berikut:

def terkecil(x: Int, y: Int) = if (x > y) y else x

Ya, cara penulisan singkat hanya berbeda pada tidak adanya kurung kurawal dan result type. Sederhana dan konsisten.

Fungsi Tanpa Argumen dan / atau Result Type

Penulisan fungsi yang tidak memiliki argumen sangat mudah, yaitu dengan mengosongkan isi dalam kurung, seperti berikut:

scala> def getRandomNumber(): Int = {
         4 // chosen by fair dice roll.
           // guaranteed to be random.
       }

Penamggilan fungsi tersebut dapat dilakukan dengan menambahkan kurung pada akhir nama fungsi, ataupun tidak.

scala> getRandomNumber
res1: Int = 4

scala> getRandomNumber()
res2: Int = 4

Bagaimana dengan fungsi yang tidak mengembalikan nilai apapun? Pada bahasa seperti C dan Java, terdapat tipe data void untuk merepresentasikan fungsi tersebut. Scala menggunakan tipe data Unit untuk menggantikan void (bahkan sebenarnya seluruh void pada Java akan dipetakan ke Unit jika kode Java digunakan bersama dengan Scala). Berikut adalah contoh kode yang menunjukkan fungsi tanpa result type:

scala> def halo() = println("Hello~")
halo: ()Unit

scala> halo()
Hello~

Local Function

Sebelum masuk ke penjelasan mengenai fungsi lokal, terlelbih dahulu kita akan menulis sebuah program untuk menambahkan dua bilangan kuadrat. Tuliskan kode berikut dan simpan ke dalam file bernama LocalFunction.scala:

def square(s: Int) = s * s
def addSquare(x: Int, y: Int) = {
  square(x) + square(y)
}

// Hasil dari 2^2 + 3^2 adalah 13
println("Hasil dari 2^2 + 3^2 adalah: " + addSquare(2, 3))

Program di atas cukup sederhana dan jelas. Awalnya, kita mendefinisikan dua fungsi, square dan addSquare, di mana square mengembalikan nilai kuadrat dari masukannya, dan addSquare menambahkan nilai kuadrat dari kedua parameternya. Kedua fungsi ini memiliki tingkatan sama, yang berarti square maupun addSquare dapat dipanggil dari manapun. Misalkan, kita dapat menambahkan baris untuk memperlihatkan nilai pangkat dua dari sebuah bilangan, sebagai berikut:

// output:
// Pangkat dua dari 4 adalah 16
println("Pangkat dua dari 4 adalah " + square(4))

dan program akan dapat berjalan tanpa masalah. Penulisan program seperti di atas memperlihatkan prinsip rancangan kode yang penting dalam pemrograman fungsional: kode program harus dipecah-pecah menjadi fungsi-fungsi kecil yang memiliki hanya satu tugas saja. Tiap-tiap fungsi yang dihasilkan biasanya akan menjadi sangat kecil. Penulisan kode seperti ini sangat memudahkan programmer, karena programmer menjadi seolah-olah memiliki banyak lego dalam berbagai bentuk yang dapat digunakan untuk membangun apapun.

Tetapi masalah yang akan muncul dengan menuliskan program seperti ini adalah penamaan fungsi. Semakin banyak fungsi yang dibuat, maka semakin banyak “polusi” penamaan fungsi. Dalam contoh yang dibuat tentunya tidak tampak langsung, karena hanya terdapat dua fungsi. Tetapi semakin besar aplikasi yang dikembangkan maka akan ada semakin banyak fungsi, yang lambat laun akan menghasilkan konflik pada penamaan. Tiap bahasa pemrograman memiliki mekanisme tersendiri untuk menyelesaikan masalah ini. Java memiliki fungsi (method) private, dan C memiliki fungsi static misalnya.

Scala menyelesaikan permasalahan yang sama dengan dua cara: fungsi private jika bekerja pada ranah PBO (Pemrograman Berorientasi Objek), dan local function jika bekerja pada ranah fungsional. Local function memiliki nama yang cukup deskriptif: fungsi lokal, fungsi yang hanya ada dalam lingkup lokal. Pengunaan fungsi lokal ini juga cukup mudah. Ganti isi dari LocalFunction.scala menjadi:

def addSquare(x: Int, y: Int) = {
  def square(s: Int) = s * s

  square(x) + square(y)
}

println("Hasil dari 2^2 + 3^2 adalah: " + addSquare(2, 3))
println("Pangkat dua dari 4 adalah " + square(4))

dan jalankan program dengan perintah scala LocalFunction.scala, maka efek dari lokalisasi fungsi akan segera terlihat melalui pesan kesalahan yang diberikan:

$ scala LocalFunction.scala
~/code/fungsi-pada-scala/LocalFunction.scala: error: not found: value square
println("Pangkat dua dari 4 adalah " + square(4))
                                       ^
one error found

Seperti yang dapat dilihat, fungsi square telah tidak dapat diakses dari luar addSquare lagi, karena square telah menjadi fungsi lokal dari addSquare. Kelebihan lainnya dari pengunaan fungsi lokal adalah segala parameter yang dimiliki oleh fungsi induk dapat diakses oleh fungsi lokalnya. Misalnya kode berikut:

import scala.io.Source

def processFile(filename: String, width: Int) = {
  val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename, width, line)
}

def processLine(filename: String, width: Int, line: String) = {
  if(line.length > width)
    println(filename + ": " + line.trim)
}

val width = args(0).toInt
for(arg <- args.drop(1))
  processFile(arg, width)

Fungsi di atas melakukan pembacaan baris pada sebuah file, dan kemudian mencetak hasil jika terdapat baris dalam file tersebut yang berukuran lebih dari width karakter. Pengunaan file di atas adalah sebagai berikut:

$ scala LocalFunction2.scala 45 LocalFunction2.scala 
LocalFunction2.scala: def processFile(filename: String, width: Int) = {
LocalFunction2.scala: def processLine(filename: String, width: Int, line: String) = {

jika ingin membaca file LocalFunction2.scala dan menampilkan baris-baris yang memiliki jumlah karakter lebih dari 45. Jika disederhanakan dengan menggunakan fungsi lokal, kode untuk processFile berubah menjadi:

def processFile(filename: String, width: Int) = {
  def processLine(filename: String, width: Int, line: String) = {
    if(line.length > width)
      println(filename + ": " + line.trim)
  }

  val source = Source.fromFile(filename)
  for (line <- source.getLines())
    processLine(filename, width, line)
}

Sangat mudah, dan tidak mengubah fungsionalitas. Tetapi perhatikan fungsi processLine sekarang. Fungsi ini masih memiliki paramter filename, width, dan line, padahal sebenarnya parameter filename dan width tidak dibutuhkan lagi! Kenapa kedua variabel tersebut tidak dibutuhkan? Seperti yang telah dijelaskan sebelumnya, fungsi yang ada di dalam fungsi lainnya dapat mengakses parameter dari fungsi induknya. Karenanya, kode di atas dapat disederhanakan lagi menjadi:

def processFile(filename: String, width: Int) = {
  def processLine(line: String) = {
    if(line.length > width)
      println(filename + ": " + line.trim)
  }

  val source = Source.fromFile(filename)
  for (line <- source.getLines())
    processLine(line)
}

val width = args(0).toInt
for(arg <- args.drop(1))
  processFile(arg, width)

Lebih sederhana bukan?

Catatan: Cupikan kode terakhir merupakan adaptasi dari contoh kode pada buku “Programming in Scala” karya Martin Ordersky. Kode asli dari buku ini dapat diambil di sini.

First Class Function

Fungsi pada Scala (dan bahasa pemrograman fungsional pada umumnya) ditempatkan pada kelas pertama. Kelas pertama artinya fungsi mendapatkan perlakukan yang sama dengan nilai-nilai dasar lain. Fungsi tidak hanya dapat dideklarasikan dan dipanggil, tetapi juga dapat diganti-ganti sesuai kemauan, dan bahkan dikirimkan sebagai nilai (misal: menjadi argumen sebuah fungsi lain). Fungsi juga tidak harus dideklarasikan untuk dapat digunakan.

Fungsi yang tidak dideklarasikan disebut dengan fungsi literal (function literal, anonymous function). Ketika sebuah fungsi literal digunakan pada saat runtime, fungsi tersebut menjadi fungsi bernilai (function value). Sederhananya, fungsi literal hanya ada dalam bentuk kode yang belum berjalan (source code), sementara fungsi bernilai merupakan manifestasi fungsi literal pada saat runtime.

Catatan: Untuk mempermudah pengertian bagi yang telah mempelajari PBO, hubungan fungsi literal dan fungsi bernilai sama antara hubungan class dengan object pada PBO. Fungsi literal adalah class, sementara fungsi bernilai adalah object.

Demonstrasi Fungsi Literal

Berikut adalah contoh fungsi literal yang menentukan apakah sebuah bilangan adalah bilangan positif atau bukan:

(b: Int) => if (b > 0) true else false

Fungsi di atas terdiri dari dua bagian, yaitu daftar parameter fungsi ((b: Int)) dan badan dari fungsi (if (b > 0) true else false). Penulisan fungsi literal pada dasarnya sama dengan deklarasi fungsi biasa, dengan perbedaan pada pengunaan => sebagai pemisah antara daftar argumen dan badan fungsi (pada deklarasi fungsi biasa, simbol pemisah yang digunakan adalah =).

Karena merupakan kelas pertama, maka fungsi tersebut dapat dimasukkan ke dalam variabel:

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

scala> cekPositif(14)
res0: Boolean = true

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

dan karena fungsi tersebut disimpan ke dalam sebuah var, maka fungsi tersebut dapat diubah:

scala> cekPositif = (b: Int) => true
cekPositif: Int => Boolean = <function1>

tetapi perhatikan bahwa tipe fungsi (Int => Boolean = <function1>) tidak dapat diubah. Kode berikut:

cekPositif = (b: Int) => 42

akan menghasilkan error seperti berikut:

scala> cekPositif = (b: Int) => 42
<console>:8: error: type mismatch;
 found   : Int(42)
 required: Boolean
       cekPositif = (b: Int) => 42
                                ^

Kenapa? Karena ketika deklarasi variabel cekPositif dikatakan memiliki tipe function1 yang memetakan Int ke Boolean. Kita tidak dapat mengubah tipe fungsi ini sama seperti kita tidak dapat mengisikan nilai Double ke variabel yang dideklarasikan sebagai Int.

scala> var integer: Int = 10
integer: Int = 10

scala> integer = 9.9999
<console>:8: error: type mismatch;
 found   : Double(9.9999)
 required: Int
       integer = 9.9999
                 ^

Catatan: Apa itu function1? Karena berjalan di atas JVM yang belum mendukung fungsi literal secara alami, maka fungsi literal pada Scala dipetakan ke dalam class tertentu pada saat berubah menjadi fungsi bernilai. class yang dipetakan adalah FunctionN, di mana N merupakan jumlah argumen fungsi. Function0 adalah fungsi tanpa parameter, Function1 memiliki satu parameter, Function2 memiliki dua parameter, dan seterusnya.

Oh ya, tentu saja fungsi literal dapat terdiri dari beberapa baris. Sintaks yang digunakan masih sama dengan fungsi biasa:

scala> val fungsiBeberapaBaris = (a: Int, b:Int) => {
         println("Fungsi ini")
         println("Terdiri dari")
         println("beberapa baris")
         println("a + b = " + a + b)
       }
fungsiBeberapaBaris: (Int, Int) => Unit = <function2>

scala> fungsiBeberapaBaris(1, 1)
Fungsi ini
Terdiri dari
beberapa baris
a + b = 11

Pengunaan Fungsi Literal

Sebagai salah satu balok pembangun utama pemrograman fungsional, fungsi literal digunakan dalam berbagai cara dan bentuk hampir pada setiap kesempatan. Contoh pengunaan fungsi literal yang paling nyata adalah pada struktur data koleksi, misalnya: List, Array, Queue, dst.

Fungsi foreach yang dijelaskan pada post sebelumnya misalnya. Fungsi ini dimiliki oleh seluruh class koleksi milik Scala. Contoh pengunaannya adalah seperti berikut:

scala> val kumpulanAngka = List(-30, -20, -10, 0, 10, 20, 30)
kumpulanAngka: List[Int] = List(-30, -20, -10, 0, 10, 20, 30)

scala> kumpulanAngka.foreach((x: Int) => println(x))
-30
-20
-10
0
10
20
30

Contoh lainnya adalah filter, sebuah fungsi yang memungkinkan kita untuk mengambil hanya elemen-elemen tertentu saja dari sebuah koleksi. Penentuan elemen apa yang akan diambil dilakukan melalui fungsi literal yang kita berikan kepada filter. Misalnya, jika ingin mengambil hanya elemen positif pada kumpulanAngka dapat dilakukan:

scala> kumpulanAngka.filter((x: Int) => x > 0)
res4: List[Int] = List(10, 20, 30)

Fungsi-fungsi seperti filter dan foreach merupakan fungsi yang sangat sering ditemukan dalam library Scala, karenanya pastikan anda mengerti konsep fungsi literal untuk dapat menggunakan Scala dengan efektif.

Bentuk Singkat Fungsi Literal

Untuk mempermudah penulisan kode, Scala memiliki fitur penebakan tipe data yang sangat canggih. Pada hampir semua kasus, penulisan fungsi literal tidak memerlukan tipe data pada argumen, sehingga penulisan fungsi literal untuk filter pada bagian sebelumnya dapat disingkat seperti berikut:

scala> kumpulanAngka.filter(x => x > 0)
res5: List[Int] = List(10, 20, 30)

dan Scala akan dapat membaca tipe data dengan benar (dalam kasus ini: kumpulanAngka merupakan List dari Int, sehingga elemennya (x) pasti bertipe Int). Bagi yang tertarik, istilah formal dari metode pembacaan tipe yang digunakan oleh Scala adalah “target typing”.

Kesimpulan

Bagian pertama menjelaskan mengenai pengertian fungsi, cara deklarasi, fungsi lokal, serta fungsi literal (yang dikenal juga dengan fungsi anonim). Semua teknik yang dijabarkan pada tulisan ini merupakan teknik-teknik yang digunakan pada pemrograman fungsional, sehingga diharapkan pembaca dapat mengerti dasar-dasar pemrograman fungsional sebelum masuk ke teknik-teknik lanjutan (closure, currying, partial function).

Akhir kata, happy coding dan sampai bertemu di tulisan selanjutnya!

comments powered by Disqus

Daftar Isi