Belajar Haskell - Purity, Laziness & Evaluasi
Episode 7 of 23

Belajar Haskell - Purity, Laziness & Evaluasi

Mendalami dua properti yang membedakan Haskell dari semua bahasa mainstream: purity yang membuat fungsi deterministik dan mudah diparalelkan, serta lazy evaluation yang memungkinkan infinite list dengan take dan cycle, termasuk kapan harus memaksa evaluasi dengan operator $! dan bang patterns untuk menghindari space leak.

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

Pendahuluan

Setelah di episode 6 kalian menguasai list dan fungsi higher-order, saatnya memahami apa yang membuat Haskell berbeda secara fundamental dari Python, Java, atau Go: purity dan laziness. Kedua properti ini bukan sekadar "fitur menarik" — keduanya mengubah cara program berperilaku, cara menulisnya, dan cara men-debug-nya.

Mengapa bab ini penting? Karena kesalahpahaman soal laziness adalah sumber paling umum space leak di aplikasi produksi, dan pemahaman purity adalah pintu menuju konteks monad di episode 11. Dua konsep ini juga yang menjelaskan mengapa Haskeller begitu percaya diri terhadap kode mereka: fungsi pure adalah fungsi yang bisa dibuktikan.

Purity: Fungsi Tanpa Efek Samping

Fungsi pure memiliki dua sifat yang bisa diuji:

  1. Deterministik: untuk input yang sama, hasilnya selalu sama.
  2. Tanpa efek samping: tidak membaca atau memodifikasi apa pun di luar fungsi.
Fungsi pure
tambahDiskon :: Double -> Double -> Double
tambahDiskon harga diskon = harga * (1 - diskon)

tambahDiskon 100 0.1 selalu menghasilkan 90.0, tidak peduli kapan, di mana, atau berapa kali dipanggil. Sebaliknya, fungsi yang membaca file, memakai waktu saat ini, atau mengubah variabel global adalah fungsi impure — hasilnya tergantung konteks.

Mengapa Purity Itu Kuat

Karena pure, fungsi Haskell punya properti yang luar biasa:

  • Mudah diuji: tanpa setup, tanpa mock, tanpa membersihkan state. Uji tambahDiskon 100 0.1 cukup.
  • Mudah diparalelkan: karena tidak ada shared mutable state, mengevaluasi dua pure function secara paralel tidak mungkin race condition.
  • Referential transparency: ekspresi bisa diganti nilainya tanpa mengubah perilaku program — ini membuat refactoring dan optimasi aman.
  • Mudah dibuktikan: kompiler (dan manusia) bisa bernalar tentang perilaku fungsi tanpa melacak jejak eksekusi global.

Di Mana Efek Samping Boleh Terjadi

Program nyata butuh I/O — membaca file, mencetak, query database. Haskell tidak melarangnya; ia mengisolasi-nya lewat tipe IO. Signatur IO adalah pengumuman: "fungsi ini berinteraksi dengan dunia luar". Aturan mainnya:

  • Kode tanpa IO = kode pure, bebas efek samping.
  • Kode dengan IO = efek samping terjadi di sana, dan hanya di sana.
Batas antara pure dan impure
pureHitung :: Double -> Double       -- aman, murni
pureHitung x = x * 2
 
impureCetak :: IO ()                  -- efek samping diisolasi
impureCetak = putStrLn "halo"

Garis pemisah yang eksplisit ini — yang di bahasa lain tidak ada — membuat kalian selalu tahu di mana "hal-hal berbahaya" terjadi. Kita bedah IO dan monad di episode 11.

Laziness: Evaluasi Hanya Saat Dibutuhkan

Haskell lazy: ekspresi tidak dievaluasi sampai hasilnya benar-benar dibutuhkan. Ini lawan dari eager evaluation (Python, Java, C) yang menghitung semuanya langsung.

Lazy dalam aksi
hampirInfinite :: [Int]
hampirInfinite = [1..]

[1..] adalah list tak hingga. Di bahasa eager, ekspresi ini langsung hang (mencoba membuat list tanpa akhir). Di Haskell, [1..] hanyalah thunk — janji nilai yang belum dihitung. Nilai sebenarnya baru dihitung saat elemen diminta:

Mengonsumsi list tak hingga
sepuluhPertama :: [Int]
sepuluhPertama = take 10 [1..]   -- [1,2,3,4,5,6,7,8,9,10]

take 10 meminta 10 elemen pertama — dan hanya 10 elemen itu yang dihitung. Inilah yang membuat ekspresi seperti [1..] menjadi sah di Haskell: komputasi ditangguhkan, dan program tidak melakukan pekerjaan yang tidak diminta.

cycle: Mengulang Tanpa Batas

cycle mengulang list selamanya, dan dengan laziness pengulangan itu tidak pernah benar-benar dibangun:

cycle - siklus tak hingga
pola :: [String]
pola = take 6 (cycle ["on", "off"])   -- ["on","off","on","off","on","off"]

Kombinasi cycle + take adalah idiom umum untuk membuat urutan periodik.

Thunk, Evaluasi, dan Konsekuensinya

Setiap ekspresi yang belum dievaluasi disimpan sebagai thunk — struktur data yang "mengingat" cara menghitungnya. Konsekuensi praktisnya:

  • Keunggulan: hanya pekerjaan yang dibutuhkan yang dilakukan. Struktur data tak hingga bisa menjadi generator murah. Dekomposisi program jadi bersih — kalian bisa membangun list raksasa secara konseptual, selama hanya mengonsumsi sebagian.
  • Risiko: thunk menumpuk. Jika kalian membangun thunk berlapis tanpa mengevaluasinya, memori terpakai untuk menyimpan "janji" — inilah space leak (kebocoran memori akibat laziness).

Contoh klasik space leak:

Space leak: thunk menumpuk
sumBesar :: [Int] -> Int
sumBesar = foldl (+) 0

Pada list besar, foldl membangun rantai thunk yang sangat dalam baru dievaluasi di akhir. Perbaikannya adalah memaksa evaluasi tiap langkah:

Perbaikan: foldl' yang strict
import Data.List (foldl')
 
sumBesarStrict :: [Int] -> Int
sumBesarStrict = foldl' (+) 0

foldl' mengevaluasi akumulator setiap langkah, sehingga memori tetap konstan. Episode 21 akan membedah masalah ini dan solusi lanjutannya secara mendalam.

Memaksa Evaluasi: $! dan Bang Patterns

Kadang kalian tahu pasti sebuah nilai akan dibutuhkan — memaksanya dievaluasi lebih awal bisa menghemat memori dan mencegah thunk menumpuk. Operator $! memaksa evaluasi argumen kanannya:

$! memaksa evaluasi
import Data.List (foldl')
 
sumSatu :: [Int] -> Int
sumSatu xs = foldl' (+) 0 xs

Di sisi lain, bang patterns (dengan ekstensi BangPatterns) memaksa evaluasi saat binding dibuat:

Bang patterns (ekstensi BangPatterns)
{-# LANGUAGE BangPatterns #-}
 
-- !x berarti x wajib dievaluasi sebelum fungsi memproses
f :: Int -> Int -> Int
f !x !y = x + y

Aturan emas: jangan menambah strictness tanpa alasan. Laziness adalah perilaku default yang sudah benar; $! dan bang patterns adalah alat bedah yang hanya dipakai saat profiling menunjukkan thunk menumpuk (dibahas di episode 21).

Warning

Kebocoran memori di Haskell jarang berasal dari "memegang data terlalu lama" — penyebabnya hampir selalu thunk yang menumpuk karena nilai tidak pernah dievaluasi. Jika program terlihat semakin lambat dan boros memori seiring waktu, cek pola foldl vs foldl' dan akumulasi hasil parsial yang menunda evaluasi.

Purity + Laziness: Alasan Komposisi Kuat

Kombinasi keduanya saling menguatkan. Karena fungsi pure (deterministik), compiler bebas menunda evaluasi tanpa mengubah hasil. Karena evaluasi lazy, struktur data tak hingga bisa "berpura-pura" ada dan dikonsumsi sebagian. Desain ini menghasilkan kode yang ditulis deklaratif (apa yang diinginkan) sementara mesin evaluasi mengurus kapan menghitungnya.

Kesalahan Umum (Common Pitfalls)

  1. Menggunakan foldl (bukan foldl') pada list besar — space leak klasik.
  2. show atau length pada list tak hinggalength [1..] akan hang, karena ia butuh seluruh list.
  3. Mengasumsikan urutan evaluasi seperti bahasa eager — di Haskell, "kapan" dievaluasi ditentukan kebutuhan, bukan urutan penulisan.
  4. Menambahkan $! di mana-mana "biar cepat" — biasanya malah melambat. Ukur dulu dengan profiler.

Penutup

Inti yang harus dibawa pulang:

  • Pure function: deterministik dan tanpa efek samping → mudah diuji, diparalelkan, dan dibuktikan.
  • Efek samping diisolasi di tipe IO; kode tanpa IO dijamin murni.
  • Laziness: evaluasi saat dibutuhkan → [1..] dan cycle legal, hanya bagian yang dipakai yang dihitung.
  • Risiko laziness: space leak dari thunk menumpuk; lawan dengan foldl', $!, dan bang patterns.
  • Gunakan strictness hanya saat profiling benar-benar menunjukkan masalah.

Di episode 8 selanjutnya kita akan membahas typeclasses & ad-hoc polymorphism — dari Eq, Ord, Show, Read, Num sampai mendefinisikan instance sendiri, lalu menaiki tangga Functor, Applicative, dan Monad yang menjadi dasar komposisi efek. Sampai jumpa di episode 8!

Belajar Haskell - Purity, Laziness & Evaluasi | Belajar Haskell