Belajar Haskell - Sintaks Dasar: Definisi & Fungsi
Episode 4 of 23

Belajar Haskell - Sintaks Dasar: Definisi & Fungsi

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.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

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

Definisi fungsi paling sederhana adalah nama, daftar parameter, =, lalu ekspresi hasil:

Definisi fungsi dasar
double :: Int -> Int
double x = x * 2
 
square :: Int -> Int
square x = x * x

double 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.

Fungsi dengan Beberapa Parameter

Fungsi dua parameter
add :: Int -> Int -> Int
add x y = x + y

Perhatikan 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.

Pola Argumen (Pattern)

Parameter tidak hanya sekadar nama — bisa langsung dipecah (deconstructed) saat pendefinisian. Inilah pattern matching pada argumen:

Pattern pada argumen
isZero :: Int -> Bool
isZero 0 = True
isZero _ = False

Definisi 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: Percabangan Tanpa if-then-else

Guards adalah cara Haskell menulis percabangan multi-kondisi dengan lebih bersih. Bayangkan fungsi menilai angka:

Guards dengan multiple kondisi
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.

Binding Lokal: where dan let

Seringkali hasil parsial dibutuhkan di banyak tempat. Haskell punya dua cara mendefinisikan nilai lokal:

where: Binding di Akhir

Binding dengan where
area :: Double -> Double -> Double
area w h = baseArea + 2 * sideArea
  where
    baseArea = w * h
    sideArea = w * h

where menempel di definisi fungsi dan bisa diakses oleh seluruh ekspresi di sisi kanan. baseArea dan sideArea di sini hanya hidup di dalam area.

let: Binding Ekspresi

Binding dengan let
area2 :: Double -> Double -> Double
area2 w h =
    let baseArea = w * h
        sideArea = w * h
    in baseArea + 2 * sideArea

let ... 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.

Aturan Layout: Indentasi Menentukan Scope

Haskell tidak memakai kurung kurawal {} untuk blok — ia memakai layout rule berbasis indentasi. Aturannya sederhana:

  • Setiap baris yang lebih dalam (indentasi lebih besar) dari baris pertama blok adalah bagian dari blok itu.
  • Baris dengan indentasi yang sama adalah sesama anggota blok.
  • Baris yang mundur keluar dari indentasi menutup blok.
Layout - indentasi menentukan scope
f x =
    let a = x + 1
        b = a * 2
    in b + a

Di 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

Komentar baris memakai -- (sampai akhir baris), dan komentar blok memakai {- ... -}:

Jenis komentar
-- Ini komentar satu baris
double x = x * 2   -- komentar di akhir baris
 
{- Ini komentar
   multi-baris -}
square x = x * x

Ada juga documentation comment khusus (-- | dan -- ^) untuk haddock — dokumentasi yang dihasilkan dari komentar — yang akan kita lihat di episode 9.

Kesalahan Umum (Common Pitfalls)

Beberapa jebakan yang hampir semua pemula temui:

  1. Lupa tanda =: f x x * 2 adalah error; selalu f x = x * 2.
  2. Kurung keliru: square (3 + 1) berbeda dari square 3 + 1.
  3. Indentasi tidak konsisten: tab dan spasi tidak boleh dicampur; pilih satu.
  4. Pattern tidak lengkap: jika isZero tanpa baris _, memanggil isZero 5 memicu error runtime non-exhaustive patterns. Selalu sediakan fallback.
Contoh pattern tidak lengkap
-- BARIS INI BERBAHAYA: tanpa kasus _ 
partialHead :: [Int] -> Int
partialHead (x:xs) = x

Memanggil partialHead [] akan crash. Kita bahas cara menulis fungsi total (yang tidak pernah crash) di episode 16 dan 17.

Penutup

Inti yang harus dibawa pulang:

  • Definisi fungsi: nama param = ekspresi; aplikasi fungsi lebih kuat dari operator.
  • Tipe Int -> Int -> Int mencerminkan currying — argumen diproses satu per satu.
  • Guards | kondisi = hasil untuk percabangan bersih; otherwise sebagai default.
  • Binding lokal: where untuk scope fungsi, let ... in ... untuk ekspresi.
  • Layout rule: indentasi menentukan scope, bukan kurung kurawal.
  • Hindari pattern tidak lengkap; selalu sediakan kasus wildcard _.

Di episode 5 selanjutnya kita akan membahas tipe data & type inferenceInt, 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!