Bagian 7: Percabangan
  |   Reading time: 12 minute(s).
Di bagian 6, kita sudah mengenal struktur data koleksi di Clojure yang bisa digunakan untuk menghimpun data heterogen beserta operasi dasar yang bisa dilakukan kepadanya. Sampai titik ini, program yang kita tulis masih berupa pemanggilan fungsi biasa yang kebanyakan hanya satu baris. Nantinya, program Clojure kita akan berkembang menjadi ratusan baris bahkan lebih. Secara default, Clojure akan mengevaluasi ekspresi dari atas sampai bawah secara berurutan. Tentunya ini tidak ideal karena kita ingin mengontrol alur evaluasi program Clojure yang kita buat. Di sinilah percabangan atau struktur kontrol berperan penting untuk membantu kita menentukan alur program berdasarkan suatu kondisi.
Di bagian ini kita akan mengenal cara membuat percabangan dalam program Clojure. Percabangan di Clojure tidak jauh berbeda dengan yang ada di bahasa pemrograman lain. Namun, perlu diingat bahwa percabangan juga merupakan ekspresi yang akan mengembalikan nilai. Jika dalam suatu cabang terdapat banyak ekspresi, maka ekspresi terakhir yang akan dikembalikan.
Tips: cara cek history ekspresi
Di bagian ini, kita akan menulis program Clojure dalam banyak baris (sebenarnya bisa dibuat satu baris tapi tidak enak dibaca). Idealnya, program tersebut ditulis di dalam file. Namun, kita masih akan bermain di REPL dulu sampai 3-4 bagian lagi. Agar tidak mengetik ulang ekspresi di REPL, gunakan tombol panah atas
↑
dilein repl
atauCTRL+↑
di Spacemacs untuk mengakses history ekspresi yang pernah kita tulis. Selain itu, untuk membuat baris baru di REPL, gunakanCtrl+Enter
.Di Spacemacs, history ekspresi disimpan dalam satu kesatuan, bukan per baris. Jadi, jika sudah menulis ekspresi yang panjangnya 10 baris, maka dengan
CTRL+↑
kita akan dapatkan 10 baris itu. Tinggal ubah bagian mana saja yang ingin dimodifikasi tanpa perlu mengetik ulang dari awal.
Pemanasan: let binding
Sebelum masuk ke materi utama percabangan, mari kita berkenalan lebih lanjut dengan fungsi let
. Sebelumnya kita sudah melihatnya di bagian 5. Fungsi let
digunakan untuk membuat binding yang bersifat lokal (gampangnya, untuk membuat variabel lokal). Binding yang ada di dalam let
juga bersifat immutable dan persistent serta tidak bisa diakses dari luar scope let
.
Berikut adalah contoh let
binding, di mana kita bind nilai 42
dengan symbol x
di dalam vector (di bahasa lain, ini sama seperti x = 42
).
user> (let [x 42] ;; bind x dan 42 dalam vector (sintaksisnya memang seperti itu).
x) ;; scope x hanya sampai sini, dan jadi nilai kembalian.
;; => 42
Setelah vector untuk binding, symbol x
sudah terasosiasi dengan nilai 42
dan kita bisa tulis ekspresi apapun yang memanfaatkan x
. Ekspresi paling akhir dalam let
akan menjadi nilai kembalian. Dalam hal ini, karena hanya ada 1 ekspresi (yaitu x
), maka nilai x
akan dikembalikan.
Kita bisa membuat banyak binding di dalam let
. Selain itu, tiap binding juga bisa digunakan untuk binding selanjutnya. Berikut contohnya:
user> (let [x 10
y (+ x 3)] ;; pakai binding sebelumnya.
x ;; x dievaluasi, tapi tidak dikembalikan.
y) ;; y jadi nilai kembalian.
;; => 13
Secara default, let
akan mengembalikan nil
jika tidak ada nilai kembalian. Nilai kembalian pun bisa kita buat sesuai dengan keinginan kita, dan tidak harus mengembalikan nilai dalam binding.
user> (let [x 4
x-kuadrat (* x x)
x-kubik (* x x-kuadrat)]
(println (str "nilai dari " x " kuadrat dan kubik adalah " x-kuadrat " dan " x-kubik))
:evaluasi-selesai) ;; mengembalikan nilai arbitrer
;; nilai dari 4 kuadrat dan kubik adalah 16 dan 64
;; => :evaluasi-selesai
Di bagian ini, let
binding akan digunakan sebagai simulasi saat kita memberikan argumen ke fungsi. Karena kita belum masuk ke materi fungsi, let
binding bisa jadi pengganti sementara.
Mari kita mulai belajar percabangan di Clojure mulai dari yang paling sederhana, yaitu fungsi if
.
Percabangan if
Fungsi if
adalah bentuk percabangan dasar di Clojure. Ia juga disebut sebagai salah satu special form. Seperti yang kita tahu, aturan evaluasi standar di Clojure adalah mengevaluasi seluruh argumen dalam suatu ekspresi. Hasil evaluasi ini akan diterapkan ke operator yang ada di sebelah paling kiri. Special form punya aturan evaluasi tersendiri yang berbeda dengan aturan standar (makanya dinamakan special form). Dalam hal ini, if
tidak akan mengevaluasi semua argumennya. Proses evaluasi dalam if
bergantung pada pengecekan kondisi.
Berikut adalah sintaksis if
:
(if <kondisi>
<ekspresi-jika-benar>
<ekspresi-jika-salah>)
Dibandingkan dengan if
di bahasa pemrograman lain, if
di Clojure hanya bisa membentuk dua percabangan untuk salah dan benar serta tidak mendukung bentuk else
. Jika ingin membuat percabangan lebih dari dua, kita harus membuat if
lagi di dalam salah satu cabang (if
bersarang, nanti kita akan melihat contohnya). Sebagai alternatif, Clojure sudah menyediakan fungsi cond
yang bisa digunakan untuk membuat banyak percabangan dan akan dibahas di bawah.
Selain itu, <ekspresi-jika-salah>
bersifat opsional dan boleh tidak dimasukkan dalam if
jika hanya memerlukan <ekspresi-jika-benar>
. Berikut adalah beberapa contoh percabangan dengan if
, di mana <kondisi>
menggunakan data literal yang bernilai true
atau false
:
user> (if true
(println "benar")
(println "salah"))
;; benar
;; => nil
user> (if false
(println "benar")
(println "salah"))
;; salah
;; => nil
user> (if nil
(println "benar")
(println "salah"))
;; salah
;; => nil
user> (if '() ;; list kosong dianggap true
(println "benar")
(println "salah"))
;; benar
;; => nil
Berikut adalah contoh if
di mana <kondisi>
adalah ekspresi yang mengembalikan boolean:
user> (if (< 4 5)
(println "4 lebih kecil dari 5")
(println "4 lebih besar dari 5"))
;; 4 lebih kecil dari 5
;; => nil
user> (if (> 4 5)
(println "4 lebih kecil dari 5")
(println "4 lebih besar dari 5"))
;; 4 lebih besar dari 5 ;; SALAH
;; => nil
Dari contoh di atas, kita bisa melihat bahwa tiap cabang hanya berisi satu ekspresi (println
untuk contoh di atas). Jika yang diinginkan adalah banyak ekspresi dalam tiap cabang, kita bisa “membungkusnya” dengan fungsi do
. Dengan do
, semua ekspresi yang ada di dalamnya akan dievaluasi berurutan dan dianggap sebagai satu blok ekspresi. Selain itu, hasil evaluasi dari ekspresi terakhir di dalam do
akan dikembalikan. Fungsi do
bermanfaat jika kita ingin melakukan banyak operasi dalam satu cabang dan biasanya operasi tersebut melibatkan side effects seperti mengirimkan data via Internet lalu menampilkan status pengirimannya.
user> (if :boleh-dikirim ;; dianggap true
(do
(println "Mengirim data...")
(println "Data telah terkirim")
:pesan-terkirim)
(do
(println "Data tidak boleh dikirim")
(println "Membatalkan operasi...")))
;; Mengirim data...
;; Data telah terkirim
;; => :pesan-terkirim
user> (if false
(do
(println "Mengirim data...")
(println "Data telah terkirim")
:pesan-terkirim)
(do
(println "Data tidak boleh dikirim")
(println "Membatalkan operasi...")))
;; Data tidak boleh dikirim
;; Membatalkan operasi...
;; => nil ;; println mengembalikan nil
Alternatif: when dan when-not
Jika yang kita pedulikan hanya satu cabang saja (benar atau salah), Clojure punya fungsi alternatif yang bisa menangani kasus tersebut, yaitu when
dan when-not
.
when
bisa digunakan jika kita ingin mengevaluasi ekspresi hanya saat kondisi bernilai true
. Jika false
, ekspresi akan diabaikan. Fungsi do
tidak dibutuhkan untuk mengelompokkan ekspresi menjadi satu karena when
akan mengevaluasi semua ekspresi saat kondisi bernilai true
.
(when <kondisi-true> <expr>)
user> (when true
(println "ini ditampilkan saat true.")
(println "akan diabaikan saat false.")
:kembalian)
;; => ini ditampilkan saat true.
;; => akan diabaikan saat false.
;; => :kembalian
user> (when false
(println "ini ditampilkan saat true.")
(println "akan diabaikan saat false.")
:kembalian)
;; => nil
Kebalikan dari when
adalah when-not
, yang akan mengevaluasi cabang hanya saat false
.
(when-not <kondisi-false> <expr>)
user> (when-not true
(println "blok ini ditampilkan saat false.")
(println "akan diabaikan saat true.")
:kembalian)
;; => nil
user> (when-not false
(println "blok ini ditampilkan saat false.")
(println "akan diabaikan saat true.")
:kembalian)
;; blok ini ditampilkan saat false.
;; akan diabaikan saat true.
;; => :kembalian
Sekarang, mari kita buat percabangan lebih dari 2 dengan if
. Contoh yang akan saya buat adalah pemeriksaan bilangan apakah ia bilangan negatif, positif, atau nol dengan fungsi pos?
dan neg?
(yang berarti akan ada 3 cabang). Percabangan tersebut akan ditempatkan di dalam let
.
user> (let [x -1] ;; bind x dengan -1
(if (pos? x) ;; apakah x positif?
:nilai-positif ;; jika benar
(if (neg? x) ;; jika salah, apakah x negatif?
:nilai-negatif ;; jika benar
:nol))) ;; jika salah
;; => :nilai-negatif
user> (let [x 0]
(if (pos? x)
:nilai-positif
(if (neg? x)
:nilai-negatif
:nol)))
;; => :nol
Dari contoh di atas, if
bisa menangani percabangan lebih dari 2. Namun, program yang kita tulis jadi kurang enak dibaca (paling tidak bagi saya). Bisa dibayangkan bagaimana jadinya jika ada lebih banyak percabangan yang harus ditulis dalam if
.
Mari kita buat contoh lagi dengan lebih banyak cabang. Kali ini saya ambil contoh perhitungan body mass index atau BMI, yang mana kategori BMI diambil dari Wikipedia seperti tabel di bawah. Percabangan yang dibuat akan menentukan kategori BMI berdasarkan nilai BMI yang kita masukkan.
Tabel BMI
Kategori | BMI (kg/m^2) |
---|---|
Underweight (Severe thinness) | < 16 |
Underweight (Moderate thinness) | 16.0 – 16.9 |
Underweight (Mild thinness) | 17.0 – 18.4 |
Normal range | 18.5 – 24.9 |
Overweight (Pre-obese) | 25.0 – 29.9 |
Overweight (Class I) | 30.0 – 34.9 |
Overweight (Class II) | 35.0 – 39.9 |
Overweight (Class III) | ≥ 40.0 |
Berikut adalah percabangan BMI dengan if
:
user> (let [bmi 18.4] ;; ganti binding untuk tes nilai lainnya
(if (< bmi 16.0)
"Underweight (Severe thinness)"
(if (<= 16.0 bmi 16.9)
"Underweight (Moderate thinness)"
(if (<= 17.0 bmi 18.4)
"Underweight (Mild thinness)"
(if (<= 18.5 bmi 24.9)
"Normal range"
(if (<= 25.0 bmi 29.9)
"Overweight (Pre-obese)"
(if (<= 30.0 bmi 34.9)
"Obese (Class 1)"
(if (<= 35.0 bmi 39.9)
"Obese (Class II)"
"Obese (Class III)"))))))))
;; => "Underweight (Mild thinness)"
Karena if
hanya punya 2 cabang, di tiap <ekspresi-jika-salah>
harus dilakukan pengecekan kebenaran lagi dengan if
. Meskipun bekerja, program BMI di atas kurang enak dibaca. Untuk menangani kasus tersebut, Clojure menyediakan fungsi cond
yang sintaksisnya lebih mudah dipahami.
Percabangan banyak dengan cond
Dengan fungsi cond
, kita bisa membuat percabangan banyak secara sederhana. Ia menerima beberapa pasang ekspresi, misalnya sepasang ekspresi kondisi-1
dan ekspresi-1
. Ekspresi kondisi-1
harus bernilai true
agar bisa mengevaluasi ekspresi-1
. Jika tidak ada ekspresi <kondisi>
yang bernilai true
, maka cond
akan mengembalikan nil
. Berikut adalah sintaksis cond
:
(cond
<kondisi-1> <ekspresi-1>
<kondisi-2> <ekspresi-2>
...
<kondisi-n> <ekspresi-n>)
Berikut adalah contoh sederhana percabangan dengan cond
yang dibungkus di dalam let
binding.
user> (let [x 10]
(cond
(< x 0) "x bernilai negatif" ;; satu pasang ekspresi
(< x 50) "x di bawah 50" ;; satu pasang ekspresi
(< x 100) "x di bawah 100")) ;; satu pasang ekspresi
;; => "x di bawah 50"
user> (let [x -5]
(cond
(< x 0) "x bernilai negatif"
(< x 50) "x di bawah 50"
(< x 100) "x di bawah 100"))
;; => "x bernilai negatif"
user> (let [x 110]
(cond
(< x 0) "x bernilai negatif"
(< x 50) "x di bawah 50"
(< x 100) "x di bawah 100"))
;; => nil
Agar bisa menangani default case, maka kita memerlukan satu pasang ekspresi lagi, yang mana <kondisi>
harus bernilai true
. Ia bisa dalam nilai apapun selain false
atau nil
. Nilai yang sering dipakai adalah :else
atau :otherwise
, yang mana kedua nilai tersebut sama-sama bernilai true
.
user> (let [x 110]
(cond
(< x 0) "x bernilai negatif"
(< x 50) "x di bawah 50"
(< x 100) "x di bawah 100"
:else "nilai di atas 100"))
;; => "nilai di atas 100"
Setelah mengenal bagaimana cond
bekerja, mari kita modifikasi contoh pemeriksaan bilangan dan BMI di atas agar menggunakan cond
.
user> (let [x -1]
(cond
(pos? x) :nilai-positif
(neg? x) :nilai-negatif
:else :nol))
;; => :nilai-negatif
user> (let [bmi 18.4]
(cond
(< bmi 16.0) "Underweight (Severe thinness)"
(<= 16.0 bmi 16.9) "Underweight (Moderate thinness)"
(<= 17.0 bmi 18.4) "Underweight (Mild thinness)"
(<= 18.5 bmi 24.9) "Normal range"
(<= 25.0 bmi 29.9) "Overweight (Pre-obese)"
(<= 30.0 bmi 34.9) "Obese (Class I)"
(<= 35.0 bmi 39.9) "Obese (Class II)"
:else "Obese (Class III)"))
;; => "Underweight (Mild thinness)"
Jika dibandingkan dengan contoh if
, percabangan dengan cond
ini terlihat lebih rapi dan enak dibaca karena tiap cabang dikelompokkan dalam satu pasang ekspresi. Jika kita nanti bertemu dengan kasus yang mempunyai cabang banyak, fungsi cond
sangat disarankan untuk dipakai.
case: Percabangan berdasarkan konstanta
Fungsi case
di Clojure mirip seperti fungsi switch-case
di bahasa C. Jika fungsi cond
memeriksa kebenaran dari kondisi, fungsi case
akan memeriksa nilai/konstanta untuk melakukan percabangan. Jika nilai/konstanta sesuai dengan yang kita inginkan, kita bisa mengevaluasi ekspresi pasangannya (sama seperti cond
). Jadi, ekspresi kondisi tidak harus true
atau false
, tapi nilai lainnya juga bisa diperiksa. Berikut adalah perbandingan cond
dan case
untuk kasus yang sama.
user> (let [x 20]
(cond
(= x 10) :sepuluh
(= x 20) :dua-puluh
(= x 30) :tiga-puluh
:else "bukan 10, 20, atau 30"))
;; => :dua-puluh
user> (let [x 40]
(cond
(= x 10) :sepuluh
(= x 20) :dua-puluh
(= x 30) :tiga-puluh
:else "bukan 10, 20, atau 30"))
;; => "bukan 10, 20, atau 30"
user> (let [x 30]
(case x
10 :sepuluh
20 :dua-puluh
30 :tiga-puluh
"bukan 10, 20, atau 30")) ;; default case
;; => :tiga-puluh
user> (let [x 50]
(case x
10 :sepuluh
20 :dua-puluh
30 :tiga-puluh
"bukan 10, 20, atau 30")) ;; default case
;; => "bukan 10, 20, atau 30"
Contoh program BMI di atas tidak cocok memakai case
karena mengharuskan kita untuk memeriksa semua nilai, yang mana jumlahnya tidak terhingga. Sehingga, cond
lebih cocok dipakai. Namun, jika kita sudah tahu nilai apa saja yang perlu diperiksa, lebih baik pilih case
daripada cond
karena case
bekerja lebih cepat. Kita bisa mencoba mengukur seberapa cepat suatu ekspresi dievaluasi dengan fungsi time
. Fungsi time
hanya menerima satu argumen, yaitu ekspresi yang akan diukur waktu evaluasinya. Mari kita bandingkan kinerja case
dan cond
untuk contoh di atas.
user> (time
(let [x 30]
(case x
10 :sepuluh
20 :dua-puluh
30 :tiga-puluh
"bukan 10, 20, atau 30")))
;; "Elapsed time: 0.025528 msecs"
;; => :tiga-puluh
user> (time
(let [x 30]
(cond
(= x 10) :sepuluh
(= x 20) :dua-puluh
(= x 30) :tiga-puluh
:else "bukan 10, 20, atau 30")))
;; "Elapsed time: 0.027662 msecs"
;; => :tiga-puluh
Dari contoh di atas, kita bisa melihat case
sedikit lebih cepat dibanding cond
. Perbedaannya tidak seberapa tapi perlu diingat bahwa itu contoh kecil. Selain lebih cepat, case
lebih singkat dan enak dibaca.
Penutup
Di bagian ini kita sudah mengenal cara mengontrol evaluasi di Clojure dengan if
, when
, when-not
, cond
, dan case
. if
cocok dipakai untuk percabangan yang tidak banyak (maksimal 2), sedangkan cond
lebih cocok untuk percabangan banyak. Selain itu, dengan case
, kita bisa mengevaluasi ekspresi berdasarkan suatu nilai yang tidak harus boolean. Dengan dukungan fungsi-fungsi percabangan di atas, kita bisa mengontrol proses evaluasi dan membuat program Clojure yang lebih kompleks.
Di bagian berikutnya, kita akan belajar cara membuat perulangan di Clojure.