Belajar Haskell - Data Processing & Streaming
Episode 14 of 23

Belajar Haskell - Data Processing & Streaming

Memproses data dalam jumlah besar tanpa memuat seluruhnya ke memori: streaming dengan Conduit dan library Streaming untuk lazy I/O, membangun internal DSL dengan bahasa ekspresif Haskell, serta parser teks dan konfigurasi yang robust dengan Megaparsec.

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

Pendahuluan

Setelah di episode 13 kalian berhasil membuat API HTTP, episode ini menjawab pertanyaan yang segera muncul begitu API kalian dipakai: bagaimana memproses data dalam jumlah besar? Log akses berukuran GB, file CSV jutaan baris, atau stream dari sistem lain — kalau diproses sebagai list penuh, program kalian akan kehabisan memori.

Mengapa bab ini penting? Karena laziness saja tidak cukup. Lazy evaluation memang menunda evaluasi, tapi thunk yang menumpuk justru bisa memicu space leak (episode 7). Streaming library memberikan resource management yang deterministik: setiap bagian data dibaca, diproses, dan dibuang tepat waktu — memori tetap konstan berapa pun ukuran input. Inilah keterampilan wajib untuk data pipeline di produksi.

Masalah: List Penuh di Memori

Pola naif — membaca seluruh file menjadi list:

Pola naif - seluruh file di memori
jumlahBarisNaif :: FilePath -> IO Int
jumlahBarisNaif fp = do
    isi <- readFile fp          -- seluruh file dimuat
    pure (length (lines isi))

Untuk file 2 GB, readFile — meski lazy di versi lama — tetap bermasalah: menyimpan isi penuh dalam thunk atau list lines. Di production, pendekatan ini adalah pintu menuju OOM. Solusinya: streaming.

Conduit: Streaming dengan Kontrol Sumber Daya

Conduit adalah library streaming paling populer di ekosistem Haskell. Modelnya punya tiga bagian:

  • Source — menghasilkan data (baca file, request HTTP).
  • Conduit (transformer) — memproses data per elemen.
  • Sink — mengonsumsi data (menulis ke file, mengakumulasi).
Conduit - hitung baris file
import Conduit
import qualified Data.ByteString as BS
 
hitungBaris :: FilePath -> IO Int
hitungBaris fp = do
    n <- runConduitRes $
        sourceFile fp           -- Source: baca file per chunk
        .| decodeUtf8C          -- byte -> Text
        .| linesUnboundedC      -- Text -> baris
        .| lengthC              -- Sink: hitung jumlah
    pure n

Alur kanan-ke-kiri (.|) membaca: "ambil sumber file, pecah jadi baris, hitung". File tidak pernah dimuat penuh — hanya buffer kecil yang beredar. Memori konstan berapa pun ukuran file.

Transformasi per Baris

Conduit unggul untuk pipeline transformasi data. Contoh memproses log dengan filter dan transform:

Pipeline transformasi Conduit
import Conduit
import qualified Data.Text as T
 
ambilError :: FilePath -> IO ()
ambilError fp = runConduitRes $
    sourceFile fp
        .| decodeUtf8C
        .| linesUnboundedC
        .| filterC (T.isInfixOf "ERROR")
        .| mapC (T.take 200)
        .| sinkFileC "errors.txt"

filterC dan mapC adalah versi streaming dari filter dan map — tetapi streaming, jadi ribuan baris pun diproses tanpa meledakkan memori. Struktur pipeline ini persis seperti rantai map/filter di episode 6, hanya kali ini diterapkan per-elemen tanpa membangun list penuh.

Tip

Memilih library streaming: Conduit untuk I/O yang sadar sumber daya (file, network) dengan ekosistem besar (termasuk integrasi database); Streaming untuk komposisi pure yang lebih elegan dan sesuai gaya lazy; dan pipes untuk abstraksi kategori yang paling murni. Semuanya menyelesaikan masalah yang sama — mulai dari Conduit karena paling sering ditemui di production.

Library Streaming: Alternatif Berbasis List

Library Streaming menawarkan gaya yang lebih dekat dengan list biasa — streaming dinyatakan sebagai struktur data monadik yang bisa dikomposisi seperti list:

Streaming library
import Streaming
import qualified Streaming.Prelude as S
 
hitungBarisStreaming :: FilePath -> IO Int
hitungBarisStreaming fp = do
    n <- S.length_ $ S.readFile fp & S.lines
    pure n

S.lines mengubah stream byte menjadi stream baris, dan S.length_ menghitungnya — semuanya streaming, dan komposisinya mirip fungsi list. Library Streaming dipilih ketika kalian ingin memakai seluruh idiom list (map, filter, fold) tetapi dengan jaminan streaming dan resource safety.

Internal DSL: Kekuatan Haskell untuk Mendefinisikan Bahasa

Haskell sangat pandai membangun internal DSL — bahasa khusus di dalam bahasa. Karena sintaksnya ekspresif (kombinator, operator, currying), DSL Haskell bisa dibaca seperti bahasa domain sendiri. Contoh nyata:

DSL: pipeline ETL sederhana
data Pipeline a
    = Sumber [a]
    | Filter (a -> Bool) (Pipeline a)
    | Transform (a -> b) (Pipeline a)   -- tipe disederhanakan

Meski contoh ini sederhana, DSL lengkap untuk workflow, konfigurasi, atau test sudah jadi produk nyata di ekosistem Haskell — salah satunya adalah Hspec itu sendiri (episode 10), yang hanyalah DSL untuk menulis spesifikasi.

Megaparsec: Parser yang Robust

Megaparsec adalah library parsing paling populer untuk teks dan konfigurasi. Parser ditulis secara composable — parser kecil digabung menjadi parser besar:

Megaparsec - parse konfigurasi
{-# LANGUAGE OverloadedStrings #-}
 
import Text.Megaparsec
import Text.Megaparsec.Char
import Data.Void
import qualified Data.Text as T
 
type Parser = Parsec Void T.Text
 
data Config = Config { port :: Int, host :: String }
    deriving (Show)
 
parseConfig :: Parser Config
parseConfig = do
    _ <- string "port:"
    p <- space1 *> decimal
    newline
    _ <- string "host:"
    h <- space1 *> some (alphaNumChar <|> char '.')
    pure (Config p h)
 
main :: IO ()
main = do
    let input = "port:3000\nhost:localhost"
    case parse parseConfig "config" (T.pack input) of
        Left err    -> putStrLn (errorBundlePretty err)
        Right cfg   -> print cfg

Keunggulan Megaparsec: error yang informatif (lokasi baris/kolom, harapan vs kenyataan), dukungan indentation untuk format seperti YAML, dan composability penuh. Ini adalah dasar validasi input yang kita pakai lagi di episode 17 untuk keamanan.

Warning

Bedakan parsing dan validasi: parsing mengubah teks menjadi struktur data; validasi memeriksa struktur tersebut terhadap aturan domain. Mulailah dengan Megaparsec untuk syntax, lalu validasi semantik (misal "port harus 1024-65535") di fungsi pure terpisah. Pisahkan keduanya sejak awal agar error tetap jelas.

Arsitektur Data Pipeline yang Disarankan

Menggabungkan semua materi, pipeline data Haskell yang sehat:

100%
  • Source membaca tanpa memuat penuh.
  • Transformasi diproses per elemen dengan fungsi pure.
  • Validasi memakai parser/validator terpisah agar error terstruktur.
  • Sink menulis hasil; error dicatat dan tidak menghentikan batch (skip-and-log).

Kesalahan Umum (Common Pitfalls)

  1. readFile untuk file besar — gunakan Conduit sourceFile atau readFileLazy dengan kesadaran penuh akan space leak.
  2. Mengumpulkan seluruh hasil streaming ke list (sinkList/S.toList_) — hanya untuk output kecil; kalau output juga besar, stream sampai ke sink.
  3. Parser yang menerima input kosong — selalu pertimbangkan eof di akhir parser agar input ekstra terdeteksi.
  4. Resource leak di stream — pakai runConduitRes (atau bracket) agar handle file ditutup otomatis, termasuk saat ada exception.
  5. String vs Text vs ByteString — untuk data besar, ByteString (binary) dan Text (unicode); String hanya untuk kode kecil.

Penutup

Inti yang harus dibawa pulang:

  • List penuh di memori adalah pintu OOM untuk data besar; gunakan streaming.
  • Conduit = Source .| transformer .| Sink; runConduitRes menjamin resource safety.
  • Streaming library menyediakan idiom list dengan jaminan streaming.
  • Haskell adalah wadah yang ideal untuk internal DSL — kombinator dan currying membuat bahasa buatan terasa natural.
  • Megaparsec menghasilkan parser yang robust dengan error informatif — fondasi validasi input di episode 17.

Di episode 15 selanjutnya kita akan membahas database & persistence — akses SQL langsung dengan postgresql-simple, ORM type-safe dengan Persistent, query SQL type-safe dengan Esqueleto, manajemen connection pool dengan resource-pool, serta migration untuk menjaga skema tetap sinkron. Sampai jumpa di episode 15!

Belajar Haskell - Data Processing & Streaming | Belajar Haskell