Menguasai sintaks dasar Haskell: mendefinisikan fungsi dengan pola argumen, percabangan memakai guards, binding lokal dengan where dan let, aturan layout berbasis indentasi yang menggantikan kurung kurawal, serta komentar -- dan {- -} beserta kesalahan umum yang sering membingungkan pemula.

Setelah di episode 3 kalian berhasil menjalankan hello world lewat tiga jalur berbeda, saatnya membangun fondasi yang sesungguhnya: cara menulis fungsi. Semua program Haskell — dari script kecil sampai server servant di episode 13 — dibangun dari blok bangunan yang sama: definisi fungsi. Setelah episode ini, kalian akan bisa membaca dan menulis kode Haskell sederhana tanpa tersesat.
Perbedaan terbesar dengan bahasa imperatif: di Haskell tidak ada perulangan for/while dan tidak ada statement. Yang ada hanyalah ekspresi dan definisi. Percabangan, perulangan, bahkan efek samping — semuanya diungkapkan sebagai ekspresi yang menghasilkan nilai. Cara berpikir inilah yang akan kita latih di episode ini.
Definisi fungsi paling sederhana adalah nama, daftar parameter, =, lalu ekspresi hasil:
double :: Int -> Int
double x = x * 2
square :: Int -> Int
square x = x * xdouble x = x * 2 berarti "fungsi double dengan satu parameter x bernilai x * 2". Tanpa keyword return — di Haskell, sisi kanan = adalah hasilnya. Panggilan fungsi juga tanpa tanda kurung: double 5 menghasilkan 10.
Urutan argumen tanpa kurung inilah yang membedakan Haskell: square 3 + 1 akan dibaca sebagai (square 3) + 1 bukan square (3 + 1). Aplikasi fungsi lebih kuat daripada operator.
add :: Int -> Int -> Int
add x y = x + yPerhatikan tipe Int -> Int -> Int — inilah currying: fungsi dua parameter sebenarnya adalah fungsi satu parameter yang mengembalikan fungsi satu parameter lagi. add 2 menghasilkan fungsi baru yang "menunggu" argumen berikutnya. Konsekuensi menariknya, kalian bisa partially apply: addOne = add 1 adalah fungsi valid yang menambahkan 1.
Parameter tidak hanya sekadar nama — bisa langsung dipecah (deconstructed) saat pendefinisian. Inilah pattern matching pada argumen:
isZero :: Int -> Bool
isZero 0 = True
isZero _ = FalseDefinisi isZero 0 = True mencocokkan argumen yang persis 0; baris kedua isZero _ = False menangkap semua kasus lain (underscore _ adalah wildcard yang mengabaikan nilai). Haskell mencoba baris dari atas ke bawah dan memakai yang pertama cocok. Episode 6 akan membedah pattern matching untuk list dan tuple secara lengkap.
Guards adalah cara Haskell menulis percabangan multi-kondisi dengan lebih bersih. Bayangkan fungsi menilai angka:
grade :: Int -> String
grade score
| score >= 90 = "A"
| score >= 80 = "B"
| score >= 70 = "C"
| otherwise = "D"Sintaksnya: setelah nama fungsi, tiap baris diawali | diikuti kondisi Boolean, = lalu hasil. Kondisi dievaluasi dari atas ke bawah; yang pertama benar menjadi hasil. otherwise adalah True — selalu benar — jadi ia berperan sebagai default/else. Guards jauh lebih mudah dibaca daripada if bertumpuk di bahasa lain.
Seringkali hasil parsial dibutuhkan di banyak tempat. Haskell punya dua cara mendefinisikan nilai lokal:
area :: Double -> Double -> Double
area w h = baseArea + 2 * sideArea
where
baseArea = w * h
sideArea = w * hwhere menempel di definisi fungsi dan bisa diakses oleh seluruh ekspresi di sisi kanan. baseArea dan sideArea di sini hanya hidup di dalam area.
area2 :: Double -> Double -> Double
area2 w h =
let baseArea = w * h
sideArea = w * h
in baseArea + 2 * sideArealet ... in ... adalah ekspresi: ia punya nilai sendiri (let x = ... in <hasil> menghasilkan <hasil> yang bisa memakai x). where adalah deklarasi yang menempel di fungsi. Untuk sekarang cukup ingat: keduanya mendefinisikan binding lokal, dan let...in bisa dipakai di mana pun ekspresi bisa berada.
Tip
Pilihan umum: gunakan where untuk binding yang dipakai di seluruh fungsi, dan let...in untuk binding yang lokal di dalam satu ekspresi atau dipakai di guards. Keduanya bisa saling menggantikan di banyak kasus — yang penting konsisten.
Haskell tidak memakai kurung kurawal {} untuk blok — ia memakai layout rule berbasis indentasi. Aturannya sederhana:
f x =
let a = x + 1
b = a * 2
in b + aDi sini a dan b harus sejajar (indentasi sama) supaya menjadi dua binding terpisah dalam satu let. Jika b salah indentasi satu spasi, GHC akan melaporkan parse error atau membaca b sebagai sesuatu yang berbeda. Kesalahan indentasi adalah penyebab paling umum error pemula — dan GHC biasanya menunjuk baris persis tempat masalahnya.
Komentar baris memakai -- (sampai akhir baris), dan komentar blok memakai {- ... -}:
-- Ini komentar satu baris
double x = x * 2 -- komentar di akhir baris
{- Ini komentar
multi-baris -}
square x = x * xAda juga documentation comment khusus (-- | dan -- ^) untuk haddock — dokumentasi yang dihasilkan dari komentar — yang akan kita lihat di episode 9.
Beberapa jebakan yang hampir semua pemula temui:
=: f x x * 2 adalah error; selalu f x = x * 2.square (3 + 1) berbeda dari square 3 + 1.isZero tanpa baris _, memanggil isZero 5 memicu error runtime non-exhaustive patterns. Selalu sediakan fallback.-- BARIS INI BERBAHAYA: tanpa kasus _
partialHead :: [Int] -> Int
partialHead (x:xs) = xMemanggil partialHead [] akan crash. Kita bahas cara menulis fungsi total (yang tidak pernah crash) di episode 16 dan 17.
Inti yang harus dibawa pulang:
nama param = ekspresi; aplikasi fungsi lebih kuat dari operator.Int -> Int -> Int mencerminkan currying — argumen diproses satu per satu.| kondisi = hasil untuk percabangan bersih; otherwise sebagai default.where untuk scope fungsi, let ... in ... untuk ekspresi._.Di episode 5 selanjutnya kita akan membahas tipe data & type inference — Int, Integer (presisi arbitrer), Double, Bool, Char, String, dan tuple, cara compiler menebak tipe dengan Hindley-Milner, serta bagaimana menulis signature eksplisit dengan ::. Sampai jumpa di episode 5!