Guard dan if

Percabangan ala Haskell dengan guard dan ekspresi if-then-else.

Daftar isi

Program nyata jarang berjalan dari atas ke bawah tanpa keputusan, dan Haskell menyediakan dua bentuk percabangan untuk itu, yaitu ekspresi if-then-else dan guard. Keduanya menghasilkan nilai pada setiap jalurnya, sehingga hasil percabangan dapat langsung disimpan sebagai definisi atau diteruskan ke fungsi lain. Topik ini membahas aturan penulisan keduanya, perbandingan penggunaannya, serta kesalahan umum yang kerap muncul saat menyusun kondisi.

Ekspresi if-then-else#

Bentuk percabangan paling ringkas adalah if kondisi then ekspresiA else ekspresiB. Kondisinya wajib bertipe Bool, dan kedua cabang wajib ada serta menghasilkan nilai bertipe sama, karena hasil ekspresi adalah nilai dari cabang yang kondisinya benar. Tidak ada varian satu cabang, sehingga Haskell tidak mengenal if tanpa else.

haskell if-ekspresi.hs
-- if-then-else adalah ekspresi, sehingga menghasilkan nilai
jenisBilangan :: Int -> String
jenisBilangan n =
  if n > 0
    then "positif"
    else if n == 0
      then "nol"
      else "negatif"

main :: IO ()
main = do
  putStrLn (jenisBilangan 12)
  putStrLn (jenisBilangan 0)
  putStrLn (jenisBilangan (-4))
text Keluaran
positif
nol
negatif

Fungsi jenisBilangan menumpuk dua if-then-else untuk memperoleh tiga kemungkinan hasil. Penulisan berlapis seperti ini sah, tetapi penumpukan berikutnya membuat cabang menjorok terus ke kanan dan cepat menjadi sulit dibaca. Ekspresi tersebut juga dapat dihitung secara langsung tanpa membuat berkas program, sebagaimana perintah berikut.

bash
# percabangan sebaris menghasilkan nilai yang dapat dicetak
ghc -e 'if even 10 then "genap" else "ganjil"'
text Keluaran
"genap"

Kondisi if boleh berupa perbandingan yang digabung dengan operator logika && dan ||, sebab hasil gabungannya tetap bertipe Bool. Ekspresi if n > 0 && even n misalnya menguji dua hal sekaligus tanpa percabangan bersarang. Karena kedua cabang wajib bertipe sama, hasilnya dapat langsung disandingkan dengan ekspresi lain, misalnya menjadi argumen fungsi atau bagian dari penjumlahan.

Letak percabangan juga bebas karena bentuk ini adalah ekspresi. if-then-else dapat muncul di dalam tubuh definisi, di dalam klausa where, maupun di antara argumen fungsi lain. Meskipun begitu, percabangan yang sama yang diulang di beberapa tempat merupakan tanda bahwa bagian itu patut diangkat menjadi fungsi tersendiri, sehingga kondisinya cukup ditulis satu kali dan diuji pada satu tempat.

Guard#

Guard menguji serangkaian kondisi secara vertikal pada definisi fungsi. Setiap kondisi ditulis setelah tanda pipa |, diuji berurutan dari atas ke bawah, dan ekspresi milik kondisi pertama yang benar dikembalikan. Guard tidak memerlukan kata kunci if, dan kondisi penangkap seluruh sisa kasus biasanya ditulis otherwise.

haskell guard-predikat.hs
-- Guard menyusun banyak kasus secara vertikal dengan kondisi yang diuji berurutan
predikat :: Int -> String
predikat nilai
  | nilai >= 85 = "sangat baik"
  | nilai >= 70 = "baik"
  | nilai >= 60 = "cukup"
  | otherwise   = "perlu latihan"

-- Fungsi jenisBilangan yang sama, kini ditulis dengan guard
kategoriBilangan :: Int -> String
kategoriBilangan n
  | n > 0     = "positif"
  | n == 0    = "nol"
  | otherwise = "negatif"

-- Guard dapat menggunakan definisi lokal dari where
statusBerat :: Double -> Double -> String
statusBerat beratKg tinggiCm
  | nilaiIndeks < 18.5 = "kurang"
  | nilaiIndeks < 25.0 = "normal"
  | otherwise          = "di atas normal"
  where
    tinggiMeter = tinggiCm / 100
    nilaiIndeks = beratKg / (tinggiMeter * tinggiMeter)

main :: IO ()
main = do
  putStrLn (predikat 92)
  putStrLn (predikat 74)
  putStrLn (predikat 55)
  putStrLn (kategoriBilangan (-4))
  putStrLn (statusBerat 45 160)
  putStrLn (statusBerat 60 160)
text Keluaran
sangat baik
baik
perlu latihan
negatif
kurang
normal

Fungsi predikat memetakan nilai ujian ke predikatnya tanpa satu pun tanda kurung, dan urutan kondisinya menjamin rentang yang benar, sebab nilai >= 70 hanya tercapai apabila kondisi pertama gagal. Fungsi statusBerat memperlihatkan guard yang bertumpu pada definisi lokal where, sehingga nilaiIndeks dihitung sekali dan digunakan oleh semua kondisi. Fungsi kategoriBilangan merupakan versi guard dari jenisBilangan, dan hasil keduanya sama persis.

Bentuk vertikal inilah keunggulan utama guard. Apabila predikat ditulis dengan if-then-else, empat kasus membutuhkan tiga penumpukan yang menjorok ke kanan, sedangkan bentuk guard menjaga setiap kasus pada kolom yang sama. Kasus baru cukup ditambahkan sebagai satu baris kondisi tanpa menyusun ulang cabang yang sudah ada, sehingga fungsi berkembang melalui perubahan yang kecil.

Urutan kondisi memerlukan perhatian yang sama dengan urutan persamaan. Kondisi yang lebih umum, seperti nilai >= 60, harus berada di bawah kondisi yang lebih spesifik, sebab kondisi pertama yang benar langsung dikembalikan. Apabila urutannya terbalik, rentang yang lebih sempit tidak pernah tercapai dan hasilnya menyesatkan tanpa satu pun pesan error dari compiler.

Memilih antara guard dan if#

Kedua bentuk dapat saling menggantikan untuk kasus dua cabang, sehingga pilihannya ditentukan oleh jumlah kasus dan letak percabangan. Tabel berikut merangkum perbedaannya.

Aspekif-then-elseguard
Bentukekspresi di dalam ekspresidaftar kondisi milik satu definisi
Jumlah cabangdua, yaitu then dan elsebebas, satu per kondisi
Kata kunciif, then, dan else wajibtidak ada, hanya pipa `
Penggunaan bersama whereperlu disusun manualseluruh kondisi melihat where

Fungsi kategoriBilangan memperlihatkan keunggulan guard untuk banyak kasus, yaitu tiga kondisi berjajar yang mudah dibaca sekilas, sedangkan versi if-then-else menumpuk cabang ke arah kanan. Sebaliknya, percabangan dua cabang yang berada di tengah ekspresi panjang lebih sesuai ditulis if-then-else karena guard memerlukan definisi tersendiri. Keduanya menghasilkan nilai, sehingga pertukaran bentuk tidak mengubah perilaku program, sebagaimana terlihat pada hasil kategoriBilangan dan jenisBilangan yang identik.

Guard melekat pada satu persamaan fungsi, sehingga fungsi dengan beberapa persamaan dapat memiliki guard pada masing-masing persamaan. Setiap persamaan tetap menuntut kondisi penangkap tersendiri agar tidak ada masukan yang terlewat. Ketika keputusannya berupa dua jalur sederhana di dalam rumus, membuat fungsi baru hanya untuk satu if justru menambah nama yang perlu diingat, sehingga bentuk if-then-else tetap menjadi pilihan yang wajar.

Kesalahan umum#

Dua kesalahan muncul paling sering pada percabangan. Pertama, if tanpa else ditolak compiler karena hasil ekspresi tidak terdefinisi apabila kondisinya salah.

haskell Contoh: if tanpa else
-- tidak-dijalankan
potongan :: Int -> Int
potongan n = if n > 0 then n

Bentuk tersebut gagal dikompilasi dengan pesan parse error, karena cabang else wajib ada pada setiap if. Perbaikannya dengan menyatakan nilai cabang else secara eksplisit, misalnya else 0, atau dengan memindahkan logika tersebut ke bentuk guard yang mampu menyatakan kasus secara terpisah.

Kesalahan kedua adalah guard tanpa kondisi penangkap. Fungsi dengan guard yang tidak mencakup semua kasus masih lolos kompilasi, tetapi masukan di luar semua kondisi memunculkan exception non-exhaustive patterns pada waktu berjalan. Perbaikannya adalah menambahkan | otherwise = ... sebagai baris terakhir, atau memastikan kondisi yang ada benar-benar mencakup seluruh kemungkinan masukan. Kebiasaan lama dari bahasa imperatif yang membolehkan cabang tanpa nilai kembalian sering menjadi penyebabnya, sehingga perlu pembiasaan baru bahwa setiap jalur Haskell wajib menghasilkan nilai.

Ringkasan#

  • if-then-else adalah ekspresi dengan kondisi bertipe Bool dan dua cabang wajib yang bertipe sama.
  • Guard menyusun banyak kondisi secara vertikal, diuji dari atas ke bawah, dengan otherwise sebagai penangkap sisa kasus.
  • Guard bekerja baik bersama where, sedangkan if-then-else cocok untuk pilihan dua cabang di dalam ekspresi.
  • if tanpa else ditolak compiler, dan guard tanpa penangkap berujung exception non-exhaustive patterns.

Referensi#