Menguasai monad sebagai alat komposisi efek di Haskell: memahami main :: IO () dan interaksi dengan dunia luar lewat >>= dan do notation, monad Maybe untuk null-safety, Either untuk error yang terstruktur, Reader Writer dan State untuk konfigurasi dan state lokal, serta peran MonadFail.

Di episode 8 kita menaiki tangga Functor → Applicative → Monad secara konseptual. Sekarang waktunya menghadapi monad yang paling nyata: IO, Maybe, Either, dan State. Episode ini adalah jembatan besar series ini — di sinilah kode Haskell berhenti menjadi "kalkulator" dan mulai menjadi program sungguhan yang membaca input, menghandle kegagalan, dan mengelola state.
Mengapa bab ini penting? Karena hampir semua pola kode Haskell produksi — membaca konfigurasi, memanggil database, memproses request HTTP — adalah komposisi monad. Setelah episode ini, kalian akan membaca kode do dengan lancar, memilih Maybe vs Either dengan tepat, dan tidak akan pernah bingung lagi dengan kata IO.
IO adalah monad yang mewakili aksi berurutan yang berinteraksi dengan dunia luar. Program Haskell selalu bermuara di main :: IO ():
main :: IO ()
main = do
putStrLn "Siapa nama kamu?"
nama <- getLine
putStrLn ("Halo, " ++ nama ++ "!")Baca do seperti urutan aksi:
putStrLn "..." adalah aksi IO yang mencetak.nama <- getLine menjalankan aksi getLine, lalu mengikat hasilnya ke nama.do.Sintaks <- (dibaca "bind") adalah sintaks gula untuk >>=:
main :: IO ()
main = putStrLn "Siapa nama kamu?"
>> getLine
>>= \nama -> putStrLn ("Halo, " ++ nama ++ "!")Keduanya identik — do hanya lebih mudah dibaca. Kunci pemahamannya: IO a adalah resep untuk menghasilkan nilai a dengan efek samping, dan >>= mengalirkan resep tersebut secara berurutan.
Aturan terpenting: nilai a di dalam IO a tidak bisa keluar ke kode pure. Tidak ada fungsi escapeIO :: IO a -> a. Inilah yang menjaga purity — dunia luar terkunci di balik gerbang IO, dan hanya fungsi bertanda IO yang boleh memasukinya.
main :: IO ()
main = do
input <- getContents
putStrLn (proses input) -- proses :: String -> String (pure)
proses :: String -> String
proses = unlines . map (++ "!") . linesPola "pure core, IO boundary": semua logika dalam fungsi pure, IO hanya di tepian (baca input, cetak output). Ini pola arsitektur yang dipegang seluruh ekosistem, dan akan kita pakai lagi di episode 13 dan 15.
Maybe a adalah Just a atau Nothing — jawaban Haskell untuk "nilai yang mungkin tidak ada":
lookupUmur :: String -> [(String, Int)] -> Maybe Int
lookupUmur nama db = lookup nama dblookup mengembalikan Just umur jika ditemukan, Nothing jika tidak. Dengan do, rangkaian operasi yang bisa gagal berjalan tanpa null check manual:
data Pengguna = Pengguna { umur :: Int }
cekUsia :: Maybe Pengguna -> Maybe String
cekUsia maybeUser = do
u <- maybeUser
if umur u >= 17
then Just "dewasa"
else NothingJika salah satu langkah menghasilkan Nothing, seluruh blok langsung menghasilkan Nothing — tidak ada "if null" bersarang seperti di Java/C#. Bahasa modern seperti Rust (Option) dan TypeScript (?) mengadopsi pola yang sama, tapi di Haskell pola ini bukan fitur tambahan — ia struktur dasar bahasa.
Either e a adalah Left e (gagal, membawa error) atau Right a (sukses, membawa nilai). Berbeda dengan Maybe, Either menyimpan detail kegagalan:
type ErrorMsg = String
divides :: Int -> Int -> Either ErrorMsg Int
divides _ 0 = Left "pembagian dengan nol"
divides x y = Right (x `div` y)Kombinasikan beberapa operasi yang bisa gagal:
hitung :: Int -> Int -> Either ErrorMsg Int
hitung a b = do
x <- divides a b
y <- divides x 2
pure yPada kegagalan pertama, rantai berhenti dan error pertama di-Left-kan. Dengan Either String, error bisa dibaca manusia; dengan Either AppError (ADT), error menjadi data yang bisa diprogram — pendekatan yang kita perdalam di episode 16.
Tip
Aturan memilih: gunakan Maybe saat kegagalan bisa diabaikan tanpa penjelasan (misal "key tidak ada"), dan Either saat kalian perlu tahu kenapa gagal (misal validasi input). Di API nyata, Either hampir selalu pilihan yang benar karena error-nya bisa di-log dan dipetakan ke response HTTP.
Tiga monad "pengiring" yang membentuk fondasi banyak library:
Reader r a mewakili komputasi yang membaca lingkungan r (konfigurasi, koneksi database) tanpa mempassing parameter secara manual:
import Control.Monad.Reader
data Config = Config { dbUrl :: String }
getDbUrl :: Reader Config String
getDbUrl = asks dbUrl
run :: Reader Config String
run = do
url <- getDbUrl
pure ("konek ke " ++ url)runReader run (Config "postgres://localhost") menghasilkan "konek ke postgres://localhost". Nilai Config "mengalir" ke semua komputasi di dalam Reader tanpa menyebutkannya berulang kali.
Writer w a membawa output kumulatif (log) bersamaan dengan nilai:
import Control.Monad.Writer
hitungDanLog :: Writer [String] Int
hitungDanLog = do
tell ["mulai"]
let r = 6 * 7
tell ["hasil: " ++ show r]
pure rHasilnya (42, ["mulai","hasil: 42"]) — nilai plus log, keduanya tanpa efek samping.
State s a mewakili komputasi dengan state s yang di-thread secara eksplisit — cara Haskell "memiliki variabel mutable" tanpa melanggar purity:
import Control.Monad.State
tambah :: Int -> State Int Int
tambah n = do
s <- get
put (s + n)
pure (s + n)
main :: IO ()
main = print (runState (tambah 5 >> tambah 3) 0) -- (8, 8)get membaca state saat ini, put menimpanya. Karena state lewat secara eksplisit, siapa pun bisa menjalankan dua state-machine yang berbeda tanpa saling mengganggu — tidak ada global state di Haskell.
Pola Just x <- ... dalam blok do disebut pattern match that can fail. GHC menuntut monad tersebut menjadi instance MonadFail:
f :: Maybe Int -> Maybe Int
f m = do
Just x <- m -- jika m = Nothing, seluruh blok Nothing
pure (x + 1)Karena Maybe adalah MonadFail, pola ini berjalan: Nothing diterjemahkan menjadi kegagalan. Ini adalah cara Haskell menangkap "asumsi yang mungkin salah" dengan aman — jika polanya tidak cocok, kegagalannya terstruktur, bukan crash.
IO — tidak ada escapeIO. Semua program bermuara di main.return tidak menghentikan eksekusi — return x di Haskell hanyalah pure x: menghasilkan nilai dalam monad, bukan "return dari fungsi".Maybe padahal butuh informasi error — ganti ke Either.x <- aksi padahal nilainya tidak dipakai; cukup aksi untuk aksi IO (), atau _ <- aksi.do tanpa monad — do membutuhkan konteks monad; jangan menulis do di fungsi pure.Inti yang harus dibawa pulang:
IO a adalah resep efek samping; nilai di dalamnya tidak bisa keluar ke kode pure.do notation adalah gula untuk >>=; <- mengikat hasil aksi.Maybe untuk nilai yang mungkin tidak ada (null-safety); Either menambahkan detail error.Reader membaca konfigurasi, Writer mengumpulkan log, State me-thread state secara eksplisit.MonadFail menangani pattern yang bisa gagal di dalam do dengan aman.Di episode 12 selanjutnya kita akan membahas concurrency & STM — membuat thread ringan dengan forkIO, sinkronisasi dengan MVar, shared state yang aman dengan Software Transactional Memory, komposisi async dengan package async, serta lazy vs strict untuk concurrent pipeline. Sampai jumpa di episode 12!