1 poin oleh GN⁺ 3 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • Mercury mengoperasikan codebase sekitar 2 juta baris Haskell di luar komentar dan sejenisnya untuk menyediakan layanan perbankan kepada lebih dari 300 ribu bisnis, serta memproses volume transaksi 248 miliar dolar AS dan pendapatan tahunan 650 juta dolar AS pada 2025
  • Nilai penggunaan Haskell di Mercury bukan terletak pada kemurnian itu sendiri, melainkan pada memasukkan pengetahuan operasional ke dalam API dan tipe, menempatkan perilaku berisiko di balik batas yang sempit, serta menjadikan jalur yang aman sebagai jalur yang paling mudah
  • Keandalan dipandang bukan sebagai pencegahan semua kegagalan, melainkan sebagai kemampuan sistem untuk menyerap variasi, dan sistem tipe berfungsi menghilangkan kelas kesalahan tertentu serta meninggalkan pengetahuan institusional sebagai dokumentasi yang dipaksakan oleh compiler
  • Mercury menggunakan Temporal sebagai framework durable execution untuk retry, timeout, pembatalan, dan pemulihan crash dalam alur kerja keuangan, serta merilis Haskell SDK hs-temporal-sdk sebagai open source
  • Nilai Haskell di produksi bukanlah memasukkan segalanya ke dalam tipe; invarian yang dapat menyebabkan kehilangan data, kesalahan finansial, atau masalah regulasi dilindungi dengan tipe, sementara kompleksitasnya dienkapsulasi dan dioperasikan bersama testing, dokumentasi, dan code review

Skala operasional Haskell di Mercury dan sudut pandang keandalan

  • Mercury mengoperasikan codebase Haskell berukuran sekitar 2 juta baris di luar komentar dan sejenisnya
  • Mercury adalah perusahaan fintech yang menyediakan layanan perbankan bagi lebih dari 300 ribu bisnis, dan pada 2025 memproses volume transaksi 248 miliar dolar AS serta pendapatan tahunan 650 juta dolar AS
  • Jumlah karyawannya sekitar 1.500 orang, dan organisasi engineering-nya terutama merekrut developer generalis; sebagian besar belum pernah menggunakan Haskell sebelum bergabung
  • Sistem ini telah berjalan selama bertahun-tahun melalui pertumbuhan cepat, situasi ketika 2 miliar dolar AS deposito baru masuk hanya dalam 5 hari saat krisis SVB, audit regulasi, serta kondisi umum maupun tidak umum dalam sistem keuangan berskala besar

Keandalan bukan pencegahan kegagalan, melainkan kemampuan menyerap variasi

  • Pendekatan keandalan tradisional berfokus pada mendata kegagalan, menambahkan pemeriksaan dan testing, serta mencari bug, tetapi itu saja tidak cukup
  • Mercury memandang keandalan sebagai kemampuan sistem untuk menyerap variasi
    • Sistem harus dapat mengalami degradasi kinerja secara anggun
    • Operator harus bisa memahami dan menyesuaikan sistem
    • Arsitektur harus membuat tindakan yang benar menjadi mudah dan tindakan yang salah menjadi sulit
  • Dalam organisasi yang tumbuh cepat, pertanyaan operasional nyata adalah apakah engineer baru bisa membaca dan memahami modul, apakah layanan ikut runtuh saat database melambat, dan apakah compiler menangkap penyalahgunaan antarmuka
  • Sistem tipe lebih dekat dengan alat bantu operasional daripada sekadar pembuktian kebenaran
    • Menghilangkan kelas kesalahan tertentu
    • Menyimpan pengetahuan institusional dalam bentuk yang bisa dibaca compiler bahkan setelah penulisnya pergi
    • Berfungsi sebagai dokumentasi yang dipaksakan secara konsisten, lebih andal daripada wiki
  • Stability engineering di Mercury bukan polisi kualitas yang memperlambat pengembangan produk, melainkan cara kolaboratif untuk menangani dampak saat fitur rusak sejak tahap awal perancangan
    • Blast radius saat gagal
    • Pekerjaan apa yang memerlukan idempotency dan bagaimana caranya
    • Bentuk rollback
    • Penanganan pekerjaan yang sedang berlangsung
    • Menilai lebih dulu sistem yang menyerap kegagalan dan yang justru memperbesarnya

Kemurnian bukan sifat bahasa, melainkan batas antarmuka

  • Kemurnian Haskell bukan berarti sama sekali tidak ada efek samping di dalamnya, melainkan lebih dekat pada gagasan bahwa antarmuka menciptakan batas yang mencegah kebocoran efek samping
  • Di balik fungsi murni dari library seperti bytestring, text, dan vector, ada implementasi internal seperti alokasi yang bisa diubah, penulisan buffer, dan unsafe coercion
  • Monad ST menggunakan perubahan in-place yang dapat diamati dan efek samping di dalam komputasi, tetapi tipe rank-2 dari runST mencegah referensi mutable yang dibuat di dalamnya bocor keluar
    runST :: (forall s. ST s a) -> a  
    
  • Di dalam, perilaku imperatif dimungkinkan, tetapi ke luar hanya hasilnya yang muncul, dan state yang dapat diubah tidak bocor melewati batas
  • Prinsip ini diterapkan ke seluruh sistem operasional
    • Lapisan database dapat secara internal menggunakan connection pooling, retry, dan state mutable
    • Cache dapat menggunakan map mutable yang konkuren
    • HTTP client dapat memiliki circuit breaker, connection pool, dan banyak bookkeeping
    • Intinya adalah membungkus perilaku berisiko dengan antarmuka sempit agar penyalahgunaan menjadi sulit
  • Dalam sistem nyata, tujuannya bukan menghindari perubahan sepenuhnya, melainkan memperjelas di mana perubahan berada dan membatasi siapa di codebase yang perlu mengetahuinya

Menjadikan tindakan yang benar sebagai yang paling mudah

  • Dalam codebase besar, sering muncul pola ketika kebenaran bergantung pada urutan tertentu atau langkah tambahan yang tidak terlihat
    • Audit log harus di-flush setelah transaksi
    • Feature flag harus diperiksa sebelum memanggil endpoint
    • Enqueue notifikasi harus dilakukan di dalam transaksi database
  • Jika pengetahuan operasional seperti ini hanya ada di wiki, dokumen onboarding, design review lama, thread Slack, atau ingatan beberapa engineer senior, pengetahuan itu cepat hilang
  • Haskell dapat mengenkodekan prosedur seperti ini ke dalam tipe sehingga tidak bisa dilupakan
  • Cara yang buruk adalah meminta orang menggunakan fungsi yang benar sambil tetap meninggalkan jalur pintas
    -- Please use this one, not the other one  
    writeWithEvents :: Transaction -> [Event] -> IO ()  
    
    -- Don't use this directly (but we can't stop you)  
    writeTransaction :: Transaction -> IO ()  
    publishEvents :: [Event] -> IO ()  
    
    • Cara yang lebih baik adalah menyusun ulang tipe agar satu-satunya jalur untuk menjalankan pekerjaan juga mencakup publikasi event
    data Transact a -- opaque; cannot be run directly  
    record :: Transaction -> Transact ()  
    emit :: Event -> Transact ()  
    
    -- The *only* way to execute a Transact: commit and publish atomically  
    commit :: Transact a -> IO a  
    
  • Di sini, sistem tipe bukan terutama membuktikan teorema mendalam tentang event, melainkan menjadikan prosedur operasional yang benar sebagai jalur yang paling mudah
  • Saat engineer baru bertanya bagaimana menulis transaksi, type signature dan API publik memberikan jawabannya, dan pengetahuan itu tetap ada meskipun engineer senior sudah pergi

Eksekusi yang Tahan Lama dan Temporal

  • Alur kerja dalam sistem keuangan tidak berhenti di dalam satu transaksi saja
    • pengiriman pembayaran
    • menunggu persetujuan partner
    • pembaruan ledger
    • notifikasi pelanggan
    • penanganan pembatalan dan timeout
    • saat partner berhasil tetapi worker mati sebelum mencatatnya
    • saat tidak ada respons karena masalah jaringan
  • Alur seperti ini membutuhkan state, retry, timeout, idempotensi, dan eksekusi yang tetap bertahan melewati crash proses maupun deployment
  • Mercury sebelumnya mengorkestrasi proses-proses ini dengan state machine berbasis database, job cron, background worker, serta penanganan retry dan timeout yang tersebar di seluruh kode
    • semuanya berjalan, tetapi rapuh, sulit dipahami, dan menjadi penyebab yang tidak proporsional dari insiden operasional
  • Temporal adalah framework durable execution Mercury, yang memungkinkan workflow ditulis seperti kode sekuensial biasa sementara platform mencatat setiap langkah ke event history
  • Jika worker crash di tengah workflow, worker lain akan me-replay prefix deterministik untuk merekonstruksi state dan melanjutkan dari titik terhenti
  • Retry, timeout, pembatalan, dan penanganan error disediakan oleh platform alih-alih diimplementasikan ulang oleh tiap tim
  • Workflow Temporal memiliki sifat yang mirip fungsi murni terhadap event history
    • workflow yang di-replay harus menghasilkan urutan perintah yang sama seperti aslinya
    • kebutuhan determinisme ini mirip dengan batasan input sama · output sama pada kode murni
    • efek samping diisolasi ke activity, yang setara dengan IO pada workflow
  • Mercury membuat Haskell SDK hs-temporal-sdk, yang membungkus Core SDK resmi Temporal dengan Rust FFI, lalu merilisnya sebagai open source
  • Pola adopsi Temporal juga dibahas dalam presentasi konferensi Temporal Replay, dan Mercury memperoleh peningkatan operasional dengan mengganti rantai cron dan state machine yang rapuh menjadi durable workflow

Domain dirancang dengan bahasa bisnis, bukan lapisan transport

  • Kesalahan umum pada sistem yang sudah berkembang adalah konsep dari sistem pemanggil bocor ke model domain
  • Jika kode yang ditulis untuk HTTP request handler kemudian dipakai ulang dalam job cron, background worker berbasis queue, atau workflow Temporal, exception HTTP seperti StatusCodeException 409 "Conflict" bisa merambat ke konteks non-HTTP
  • Job cron tidak memiliki pemanggil yang menunggu respons 409, dan status code membawa makna bisnis ke lapisan yang salah
  • Solusinya adalah memodelkan domain error sebagai tipe domain
    • saldo tidak cukup harus berupa InsufficientFunds
    • permintaan duplikat harus berupa DuplicateRequest
    • timeout partner harus berupa PartnerTimeout
  • Letakkan lapisan konversi tipis di tiap boundary
    data PaymentError  
      = InsufficientFunds  
      | DuplicateRequest RequestId  
      | PartnerTimeout Partner  
    
    toHttpError :: PaymentError -> HttpResponse  
    toHttpError InsufficientFunds       = err402 "Insufficient funds"  
    toHttpError (DuplicateRequest _)    = err409 "Duplicate request"  
    toHttpError (PartnerTimeout _)      = err502 "Partner unavailable"  
    
    toWorkerStrategy :: PaymentError -> WorkerAction  
    toWorkerStrategy InsufficientFunds    = Fail "Insufficient funds"  
    toWorkerStrategy (DuplicateRequest _) = Skip  
    toWorkerStrategy (PartnerTimeout _)   = RetryWithBackoff  
    
  • Concern lapisan transport harus tetap berada di tepi, dan model domain tidak boleh membawa-bawa HTTP status code meskipun dipanggil dari web handler, CLI, job cron, background worker, atau workflow engine

Biaya encoding tipe dan titik keseimbangannya

  • Memasukkan invariant ke dalam tipe itu kuat, tetapi ada biayanya berupa biaya kognitif, kekakuan, dan kesulitan saat kebutuhan berubah
  • Jika pelanggaran dapat menyebabkan kehilangan data, kesalahan finansial, masalah regulasi, atau insiden on-call, maka biaya encoding tipe layak dibayar
  • Jika alasannya hanya karena itulah cara sekarang, atau karena ingin mencoba teknik level tipe, kemungkinan besar itu justru membuat codebase lebih sulit diubah
  • Sisi yang meng-encode terlalu banyak

    • state ilegal tidak bisa direpresentasikan dan domain dimodelkan dengan setia dalam tipe
    • perubahan aturan bisnis berujung pada perubahan tipe yang menembus 50 modul sehingga refactor menjadi panjang
    • engineer baru jadi sulit memahami type signature
  • Sisi yang tidak meng-encode apa pun

    • tipe menjadi mendekati String, IO (), atau dalam kasus terburuk Dynamic
    • kode mudah diubah, tetapi tidak ada kontrak, dan makna bergantung pada ingatan penulis sebelumnya
    • ketika penulisnya pergi, menjadi sulit mengetahui mengapa sistem itu berjalan atau tidak berjalan
  • Patokan yang berguna

    • invariant yang mencegah kerusakan senyap lebih baik dimasukkan ke dalam tipe
      • transaksi di-commit tanpa event
      • pembayaran diproses tanpa audit log
      • transisi state yang tampak mungkin tetapi secara makna tidak mungkin
    • invariant yang gagal dengan keras mungkin cukup ditangani dengan pemeriksaan runtime yang punya pesan error yang baik
      • respons 500
      • assertion gagal
      • ketidakcocokan tipe pada boundary JSON
    • dorongan untuk memodelkan seluruh domain ke dalam tipe harus ditahan
      • domain memiliki pengecualian, aturan kompatibilitas lama, aturan yang saling bertentangan, dan perilaku khusus untuk pelanggan tertentu
    • tipe adalah alat bukan hanya untuk compiler, tetapi juga untuk tim
      • ia harus membentuk lapisan pertahanan bersama testing, dokumentasi, code review, contoh, dan playbook
    • Di internal Mercury ada juga library yang memakai perangkat level tipe yang kompleks seperti GADT, type family, dan phantom type yang melacak transisi state
    • kompleksitas seperti ini dibutuhkan pada mekanisme yang bila salah bisa membuat uang berpindah secara keliru atau invariant regulasi rusak
    • kuncinya adalah mengenkapsulasi kompleksitas
    • modul yang mengimplementasikan state machine level tipe harus dimiliki oleh segelintir penulis yang benar-benar memahaminya dan dilengkapi testing yang cukup
    • API untuk pihak yang menggunakannya harus terlihat seperti beberapa fungsi dengan tipe biasa
    • product engineer harus bisa memanggilnya dengan aman tanpa perlu mengetahui perangkat pembuktian level tipe di dalamnya
    • jika dalam code review sebuah PR yang menyentuh modul lain dipenuhi anotasi tipe hasil salin-tempel demi menenangkan compiler, itu tanda bahwa abstraksi bocor melewati boundary

Merancang untuk introspeksi

  • Jika keandalan adalah kemampuan beradaptasi, kemungkinan introspeksi adalah salah satu cara untuk memperoleh kemampuan itu
  • Operator tidak bisa mengoperasikan sesuatu yang tidak dapat mereka lihat, dan tim sulit beradaptasi dengan sistem yang bagian dalamnya buram
  • Haskell tidak memiliki monkey patching, sehingga sulit mengganti HTTP client internal pustaka saat runtime atau mengganti pemanggilan database dengan fungsi yang menghasilkan span OpenTelemetry
  • Rust memiliki keterbatasan yang sama, tetapi ekosistem Rust telah berkumpul pada pola middleware tower, sedangkan ekosistem Haskell terpecah ke beberapa pendekatan
  • Jika pustaka hanya mengekspos kumpulan fungsi tingkat atas yang konkret, maka untuk menambahkan instrumentasi Anda harus membungkusnya dalam modul baru dan berharap orang meng-import modul itu alih-alih modul aslinya
  • Record fungsi

    • Solusi yang paling sering dipakai adalah mengekspos record fungsi alih-alih fungsi konkret
      -- A concrete module gives you no leverage:  
      sendRequest :: Request -> IO Response  
      -- A record of functions gives you all of it:  
      data HttpClient = HttpClient  
      { sendRequest :: Request -> IO Response  
      , getManager  :: IO Manager  
      }  
      
    • Dengan cara ini, sendRequest bisa dibungkus dengan pengukuran waktu lalu mengembalikan HttpClient baru
    • Concern lintas fungsi seperti fault injection untuk pengujian, penggantian mock, retry, tracing, rewrite request, dan perilaku per tenant bisa ditambahkan saat runtime
    • Pola yang membuat transformasi perilaku dapat dikomposisikan, seperti type Middleware = Application -> Application milik WAI, sangat berguna secara operasional
  • Interceptor yang dapat dikomposisikan dengan Monoid

    • Tipe middleware dan interceptor biasanya dapat memiliki instance Semigroup dan Monoid
    • Middleware pada WAI adalah endomorphism, dan endomorphism membentuk monoid di bawah komposisi dan id
    • Record hook interceptor bisa dikomposisikan per field, sehingga concern seperti tracing, timeout, dan rewrite task queue dapat digabung dengan mconcat tanpa plumbing terpisah
      appTemporalInterceptors =  
      mconcat  
        [ retargetingInterceptor  
        , otelInterceptor  
        , sentryInterceptor  
        , sqlApplicationNameInterceptor  
        , loggingContextInterceptor  
        , statementTimeoutInterceptor  
        , teamNameInterceptor  
        , clientExceptionInterceptor  
        , workflowTypeNameInterceptor  
        ]  
      
    • Tiap interceptor menangani hanya satu concern dalam modul terpisah, melakukan override hanya pada field yang diperlukan dari mempty, dan urutannya dinyatakan jelas dalam daftar
  • Effect system

    • Effect system seperti effectful, polysemy, fused-effects, dan cleff juga menyediakan jalur lain
    • Operasi yang tersedia didefinisikan sebagai tipe effect, dan interpreter untuk production, testing, atau tracing dapat diganti di titik pemanggilan
    • Effect dapat dicegat untuk mencatat metrik atau menyuntikkan delay sebelum dikirim kembali ke handler yang sebenarnya
    • Kekurangannya adalah bertambahnya perangkat seperti daftar effect di level tipe, stack handler, dan error tipe yang sulit
    • Record fungsi cukup sederhana hingga engineer baru bisa memahaminya hanya dalam satu sore
  • Contoh positif dari persistent

    • SqlBackend milik persistent adalah record fungsi seperti connPrepare, connInsertSql, connBegin, connCommit, dan connRollback
    • Saat menambahkan instrumentasi OpenTelemetry, field yang relevan dapat dibungkus untuk menempelkan tracing span ke semua operasi database
    • Visibilitas pada lapisan database diperoleh tanpa fork dan hampir tanpa perubahan source code
  • Pustaka yang sulit dioperasikan

    • Mercury hampir tidak menggunakan binding klien web API yang dipublikasikan di Hackage
    • Jika binding pihak ketiga melakukan pemanggilan HTTP dengan fungsi konkret, maka tracing, timeout sesuai SLO, simulasi gangguan partner, atau menjelaskan celah 400 ms dalam trace menjadi sulit
    • Karena itu mereka menulis klien sendiri dan membuatnya dapat diamati sejak awal
  • Biaya dari ekosistem kecil

    • Sebagian pustaka Haskell bukan ditinggalkan, tetapi tetap seperti infrastruktur publik tanpa pihak yang jelas-jelas bertanggung jawab dan cepat memperbaikinya
    • Antarmuka lama tetap dipertahankan, dan laju penerimaan desain baru terkait observability, desain boundary, dan operability bisa lambat
    • http-client secara langsung hanya mendukung HTTP/1.1, cukup berguna, tetapi pada titik tertentu mungkin memerlukan solusi обход

Kebutuhan operasional untuk penulis paket

  • Penulis pustaka harus menyediakan jalan keluar seperti record fungsi, tipe effect, atau callback agar pengguna dapat menyuntikkan perilaku tanpa memodifikasi source code
  • Menambahkan hs-opentelemetry-api sebagai dependensi dan menaruh span di sekitar operasi inti IO saja sudah membantu pengguna yang menjalankan pustaka itu di production
    • Paket API ini konservatif terhadap breaking change, dan dirancang untuk bekerja secara inert jika aplikasi tidak menginisialisasi OpenTelemetry SDK
    • Overhead performanya diminimalkan, dan tidak menimbulkan exception tak terduga atau logging dari aplikasi pengguna
    • Footprint dependensinya masih belum sekecil yang diinginkan dan sedang diperbaiki
  • Jangan menulis log langsung dari kode pustaka
    • Alih-alih meng-import framework logging dan menulis langsung ke stdout atau stderr, pustaka harus menyediakan callback, parameter logger, atau tipe data pesan log yang bisa dirutekan oleh pemanggil
    • Ke mana log dikirim adalah keputusan yang termasuk dalam lingkungan operasional aplikasi
    • Mercury mengirim pipeline log terstruktur ke observability stack, dan jika pustaka menulis langsung ke stderr, itu akan memerlukan plumbing terpisah dari aliran JSON lines
  • Mengekspos modul .Internal juga bisa dipertimbangkan
    • Kekhawatiran bahwa pengguna akan bergantung pada API internal dan membuat refactor menjadi sulit adalah hal yang valid
    • Namun, keyakinan bahwa API publik sudah mencakup semua use case jarang benar-benar dapat dibenarkan
    • Modul .Internal dengan peringatan stabilitas yang eksplisit bisa lebih baik daripada pengguna harus mem-fork dan mem-vendor paket
    • containers, text, dan unordered-containers adalah contoh bagus pendekatan ini di ekosistem Haskell
    • Namun, jika pengguna diam-diam memakai modul internal untuk menyelesaikan kebutuhannya, feedback tentang kekurangan API publik bisa berkurang

Hal-Hal yang Tidak Dimasukkan ke dalam Tipe

  • Bahkan Haskell untuk production pun punya bagian yang tidak indah
  • unsafePerformIO digunakan di dalam library yang kita andalkan sehari-hari
    • bytestring dan text secara internal mengalokasikan buffer yang dapat diubah, menulis ke sana, lalu melakukan freeze untuk membuat hasil
    • Tipe tidak memberi tahu apa yang terjadi selama proses pembentukan
    • Batasannya dijaga melalui konvensi, penalaran yang hati-hati, dan code review
  • Jika alternatif yang type-safe membuat biaya performa atau kompleksitas menjadi terlalu besar, Anda mungkin juga perlu menulis kompromi semacam ini sendiri
    • Invarian yang tidak diperiksa oleh tipe harus didokumentasikan
    • Pertahankan ketidaknyamanannya, dan tinjau ulang secara berkala apakah alternatif yang type-safe sudah menjadi praktis
    • Haskell production bukan tentang ketiadaan kompromi, melainkan isolasi yang disiplin terhadap kompromi
  • Banyak library Haskell di Hackage memiliki sedikit atau tidak memiliki test sama sekali
    • Gagasan bahwa “kalau bisa di-compile berarti berjalan” kadang benar untuk kode kecil yang murni dan bertipe kuat
    • Namun itu hampir tidak pernah benar untuk kode yang berat pada IO, integrasi dengan sistem eksternal, atau kode yang bug-nya ada pada makna, bukan pada strukturnya
  • Tipe bisa menyatakan bahwa sesuatu mengembalikan Either ParseError Transaction, tetapi tidak bisa menyatakan hal-hal berikut
    • apakah field amount di-parse sebagai sen atau sebagai dolar
    • apakah partner API menafsirkan field yang dihilangkan dan field null secara berbeda
    • apakah logika retry menyebabkan penagihan ganda pada jendela waktu tertentu di tahun kabisat
  • Di production, kita membangun sistem di atas library semacam ini, mewarisi asumsi yang belum diverifikasi, sehingga harus melengkapinya dengan integration test pada lapisan kita sendiri
  • Kompromi seperti orphan instance, partial function yang dipercaya total dalam konteks tertentu, error yang dijanjikan tidak akan pernah tercapai, wrapper FFI yang canggung, dan exception hierarchy buatan tangan juga akan menumpuk
  • Tujuannya bukan kemurnian moral, melainkan memastikan lewat code review, dokumentasi, contoh, dan test bahwa kita tahu di mana setiap kompromi berada, mengapa itu dibuat, dan apa yang akan rusak jika dihapus

Apakah Haskell Layak Dipakai di Production

  • Haskell bukan pilihan yang cepat sejak hari pertama
    • Ekosistem saat ini tidak bisa langsung menyediakan lingkungan pengembangan hot-reloading yang serba tersedia seperti Next.js atau Rails
    • Library yang dibutuhkan bisa jadi tidak ada, atau kalau ada pun mungkin dipelihara oleh satu orang di waktu luangnya
    • Pesan error kadang bisa sangat sulit dipahami
  • Masalah perekrutan dibesar-besarkan
    • CTO Mercury, Max Tagher, pernah mengatakan secara terbuka bahwa backend Haskell engineer adalah peran yang paling mudah direkrut di seluruh Mercury
    • Permintaan untuk pekerjaan Haskell lebih besar daripada pasokannya, sehingga dinamika perekrutan yang biasa menjadi terbalik
    • Mercury merekrut baik orang dengan pengalaman Haskell yang mendalam maupun yang sama sekali belum punya pengalaman, dan yang terakhir dibuat produktif melalui program pelatihan 6–8 minggu
    • Jika Anda butuh 100 ahli Haskell besok, masalah ukuran talent pool memang nyata; tetapi jika Anda mau merekrut developer generalis yang baik lalu mengajari mereka, masalah itu jauh kurang nyata
  • Risiko perekrutan yang lebih besar bukan ukuran talent pool, melainkan temperamen
    • Haskell menarik kaum idealis yang peduli pada ketepatan dan abstraksi, senang membaca paper, dan gemar mempertanyakan asumsi yang sudah ada
    • Kekuatan ini bisa menjadi beban production jika tidak dikendalikan
    • Mencoba menulis ulang lapisan database dengan encoding relational algebra baru di level tipe, menolak merge karena seseorang tidak memakai Text alih-alih String dalam throwaway script, atau menyeret setiap desain ke arah total rewrite ala paper terbaru akan memperlambat tim
  • Haskell production membutuhkan budaya pragmatisme
    • Sistem tipe adalah perkakas listrik, bukan agama
    • Menganggap masalah yang sudah punya solusi bagus sebagai kesempatan untuk menciptakan mekanisme baru tidak cocok untuk production
  • Keuntungannya muncul seiring waktu
    • Refactor yang mungkin memakan waktu berminggu-minggu di codebase bertipe dinamis bisa selesai dalam hitungan jam setelah perubahan tipe, karena compiler memberi tahu semua call site
    • Engineer baru dapat membaca type signature dan memahami kontrak sebuah modul
    • State yang mustahil benar-benar tidak dapat direpresentasikan, sehingga incident production mungkin tidak terjadi
  • Mercury melihat pengembalian investasinya muncul bukan dalam hitungan tahun, melainkan bulan
    • Terutama di layanan keuangan, biaya bug integritas data diukur bukan dengan keluhan pengguna, melainkan dengan teguran regulator dan uang milik orang lain
    • Sistem tipe tidak menghilangkan risiko, tetapi menyediakan alat yang membuat lebih sulit untuk secara tidak sengaja memasukkan risiko ke dalam codebase yang tumbuh cepat
  • Nilai Haskell di production bukanlah peluru perak atau gerakan moral, melainkan seperangkat alat yang kuat yang memungkinkan tim dengan tingkat kemahiran Haskell yang beragam untuk menjaga perangkat berbahaya tetap berada dalam pagar pembatas, melestarikan pengetahuan operasional, dan menjadikan jalur yang aman sebagai jalur yang mudah

1 komentar

 
GN⁺ 3 jam lalu
Komentar Hacker News
  • Memang benar Haskell termasuk salah satu bahasa yang paling kuat untuk memaksakan hal seperti ini lewat tipe, tetapi pola yang sama juga cukup efektif di Rust dan TypeScript
    Saya juga suka pendekatan untuk mencegah bug otorisasi yang jelas dan berulang di web app dengan alur seperti User -> LoggedInUser -> AccessControlledLoggedInUser
    Menurut saya, pola ini sangat kurang digunakan di industri

    • Ini bukan hanya berlaku untuk Rust atau TypeScript, sebenarnya hampir semua bahasa bisa melakukannya
      Jika secara keamanan Anda perlu membedakan string sebelum/sesudah di-escape, bahkan di bahasa bertipe dinamis Anda bisa membungkusnya dengan kelas Escaped dan menyediakan fungsi seperti escape(str)->Escaped, dangerouslyAssumeEscaped(str)->Escaped
      Ada biaya performa jadi memang perlu kompromi, tetapi ini memungkinkan
      Cara lain adalah Application Hungarian, hanya saja ini lebih bergantung pada disiplin programmer daripada compiler: https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
    • Ini rasanya lebih dekat ke masalah affordance daripada tipe sistem itu sendiri
      Misalnya di C# juga sangat mungkin dilakukan, tetapi noise visualnya jadi lebih besar daripada definisi tipe aslinya
    • Rust dan TypeScript tentu juga sangat dipengaruhi oleh Haskell
      Hanya saja, untuk menghindari efek seperti “monad terdengar menakutkan jadi kita harus menulis tutorial”, mereka cenderung tidak menyebutkannya secara eksplisit dan memakai nama yang berbeda
      Pengaruhnya lebih besar dari hal seperti type class daripada monad
    • Saya tidak terlalu yakin ini benar-benar bekerja dengan baik di TypeScript
      Karena tidak ada nominal typing, untuk membuat sesuatu seperti newtype yang membungkus tipe primitif, Anda harus mengingat mantra yang cukup hacky
      Dalam pengalaman saya, OCaml lebih kuat daripada Rust untuk memaksakan keamanan tipe seperti ini
      Ia lebih ekspresif dengan GADT, lebih nyaman dengan polymorphic variant dan object type/record row type, serta punya module system dan functor
      Di domain yang cukup dengan garbage collection, Anda juga bisa menghindari batasan abstraksi dan kesulitan yang muncul karena borrow checker Rust
    • Ini sama seperti gagasan “buat keadaan yang salah menjadi tak bisa direpresentasikan”: https://news.ycombinator.com/item?id=40150159
  • Saya sangat suka bekerja dengan Haskell selama beberapa tahun
    Bukan sesuatu yang saya cari sengaja, tetapi kebetulan kesempatannya datang, dan itu menarik sekaligus merangsang secara intelektual
    Namun sayangnya, bahkan setelah 3 tahun hanya memakai Haskell, produktivitas saya di Rust tetap dengan mudah dua kali lipat dibanding di Haskell
    Haskell punya lebih banyak jebakan yang harus Anda tahu dan hindari sejak awal, dan tergantung penulisnya kadang sulit dicerna sampai terasa hampir seperti bahasa yang hanya untuk dibaca
    Toolchain-nya juga sering dipadukan dengan Nix, yang sendiri merupakan monster yang rumit, ekstensi bahasanya terasa tersebar di mana-mana
    File Cabal juga kurang bagus, dan butuh waktu untuk terbiasa dengan error compiler-nya

    • Cukup mengejutkan, pengalaman saya hampir kebalikannya
      Di produk terakhir, kami mulai memindahkan backend dari Typescript ke Rust karena lelah dengan crash
      Sekarang saya menganggap itu salah satu kesalahan teknis terbesar yang pernah saya buat, karena produktivitasnya jadi sangat lambat
      Contoh waktu yang terbuang khusus di Rust: menulis higher-order function yang membuka koneksi database, melakukan sesuatu, lalu menutupnya itu sepele di Haskell, TypeScript, JavaScript, C++, dan PHP, tetapi di Rust pada praktiknya mustahil sampai saya tanya teman-teman yang ahli Rust lalu akhirnya menyerah
      Saya juga beberapa kali mencoba refactor, menghabiskan seharian memperbaiki type error, lalu menemukan error di file paling atas, dan akhirnya menyimpulkan seluruh refactor itu mustahil karena bagian fundamental dari desain, jadi semuanya dibatalkan
      Selain itu, Rust adalah satu-satunya bahasa modern yang bisa saya pikirkan di mana menggunakan nilai sebagai interface, alih-alih tipe konkret, berada di antara teknik lanjutan dan tidak mungkin tergantung situasinya
      Karena itu saya sampai pada kesimpulan bahwa kode aplikasi, yaitu bukan kode sistem atau kode library, pada dasarnya tidak boleh ditulis dengan Rust
    • Saya penasaran apakah produktivitasnya memang 2x secara keseluruhan, atau ada juga bagian di Rust yang membuat Anda kurang produktif
      Dan saya juga penasaran maksud “hanya untuk dibaca” itu apa
  • Berbeda dari persepsi umum, saya rasa fakta bahwa Mercury memilih Haskell dan para pemimpin awalnya punya pengalaman mendalam dengan Haskell mungkin berperan cukup besar dalam keberhasilannya
    Dari sudut pandang pelanggan Mercury, perusahaan ini adalah salah satu perusahaan inti dalam toolbox saya, dan saya sulit menepis kesan bahwa pemilihan Haskell membuat progres, pengembangan, dan perjalanan mereka secara keseluruhan menjadi lebih baik
    Tentu klaim seperti ini bisa dibuat untuk kebanyakan bahasa, dan bukan berarti bahasa fungsional seperti Haskell adalah formula sukses
    Tetapi membuat keputusan sengaja seperti itu sebelum era “vibe coding” dan LLM terasa sangat visioner, dan menurut saya hasilnya juga berpadu dengan budaya engineering yang dijelaskan rinci dalam tulisan itu

    • Bisa jadi faktor keberhasilannya justru fokus fintech yang berorientasi startup dan kemampuan eksekusi
      Saya juga suka budaya teknis yang baik, tetapi saya pernah melihat perusahaan dengan budaya teknis hebat mati karena fokus bisnis yang buruk
      Bahkan bisa jadi budaya fintech ala startup itu sendiri yang melahirkan budaya teknis yang baik
      Karena mereka tidak berawal sebagai bank, mereka tidak perlu se-konservatif itu seperti misalnya SVB, dan tidak perlu terintegrasi dengan tumpukan teknologi kuno yang mengerikan
      Saya senang mereka berhasil dengan Haskell, tetapi seperti Jane Street dengan OCaml, menurut saya pilihan bahasa dari sisi bisnis hampir bersifat kebetulan, walau perusahaan ingin Anda percaya sebaliknya
      Tapi saya penasaran frontend mereka pakai apa. Mungkin Haskell ini semuanya ada di backend
    • Merekrut generalis yang belum punya pengalaman dengan bahasa tersebut mungkin justru membantu
      Karena budaya dan gaya bisa ditanamkan ke orang-orang baru sejak awal
      Sebelum era vibe coding, kebanyakan dari mereka juga mungkin tidak akan langsung nyemplung dan hacking tanpa arahan apa pun
    • Saya merasakan bahwa semua hal di aplikasi itu memang bekerja dengan baik
      Kalau pindah dari layanan lain, rasanya benar-benar memuaskan
  • Sahabat dekat saya bekerja di perusahaan ini, dan bahkan dari luar budaya engineering-nya terlihat bagus
    Saya rasa Haskell adalah alat yang tepat untuk pekerjaan ini dan mereka memanfaatkan kekuatannya dengan baik, tetapi saya juga merasa sebagian besar keberhasilannya mungkin hanya karena perusahaannya secara umum dikelola dengan baik

    • Kesan saya saat membaca artikelnya juga begitu
      Rasanya penulis ini akan tetap bisa menjalankan organisasi engineering yang sukses apa pun bahasa yang dipakai
    • Ini juga tidak bertentangan dengan anggapan umum bahwa memakai bahasa pemrograman fungsional menyaring pool talenta/kandidat dengan kualitas lebih tinggi
  • Saya sedang membaca Real-World OCaml sekarang, dan walau beberapa hal sudah saya tahu, saya sedang belajar lebih banyak tentang functional programming
    Rasanya dengan functional programming Anda bisa membuat potongan software yang sangat kokoh
    Tetapi saya juga galau
    Saat ini backend produk berjalan dengan NiceGUI dan menjalankan perannya dengan baik
    Kodenya masuk akal, MVVM, dan hal terpentingnya adalah terhubung ke websocket per pelanggan untuk mengonsumsi data dan menampilkan analisis
    Jumlah pelanggannya tidak akan banyak, dan pengunjung situs webnya mungkin hanya puluhan sampai paling banyak ratusan orang
    Saya juga ingin REPL atau hot reload, tetapi saya tahu saat fitur bertambah, seperti panel manajemen pengguna, analitik tambahan, dan seterusnya, functional programming mungkin cocok untuk transformasi data pipeline
    Hanya saja Haskell maupun OCaml adalah bahasa statis
    Jika nanti saya ingin sesuatu yang dinamis sambil tetap bisa tumbuh dan skala, Clojure atau Elixir tampaknya pilihan bagus
    Di saat yang sama, saya takut kalau suatu hari perlu refactor semuanya malah rusak
    Saat ini saya memakai Python dan Mypy, dan frontend dihasilkan dari backend oleh NiceGUI

    • Saya tidak tahu soal OCaml, tetapi di Haskell Anda bisa me-reload web app yang sedang dikembangkan dengan sangat cepat lewat ghci/cabal repl
      Sejujurnya saya rasa banyak pengguna Haskell kurang memanfaatkan ini
  • Saya pernah mengerjakan sistem serupa dengan bahasa yang relatif niche, Scheme lalu kemudian Racket, dan walaupun skalanya membesar, tim kecil tetap bisa memeliharanya lama dan mempertahankan kecepatan tinggi
    Kami tidak membuat banyak bug, dan biasanya bisa menambahkan fitur dengan sangat cepat
    Misalnya kami yang pertama kali mendapatkan sertifikasi tertentu untuk meng-host data sensitif di AWS
    Kadang penambahan fitur memang melambat karena kami harus membangun dari nol sesuatu yang di platform populer bisa diselesaikan dengan komponen siap pakai
    Tetapi setelah jadi, biasanya hasilnya bekerja dengan baik, lalu kami kembali ke kecepatan semula, tanpa terbebani kembung dan kompleksitas dari puluhan framework siap pakai
    Karena kami mengendalikan sendiri platform yang tetap manageable, kami juga bisa cepat berpindah ke AWS saat kebutuhan itu muncul
    Sistemnya sejak awal juga punya rahasia arsitektur untuk data kompleks dan interaksi web, dan ini membantu banyak fitur dikembangkan dengan cepat sekaligus terus mendorong ke arah yang cerdas setelahnya
    Perbedaan dengan fintech Haskell ini adalah ukuran timnya sangat kecil
    Dalam satu waktu, engineer software-nya hanya 2–3 orang, dan ada satu orang yang menangani seluruh operasi
    Jadi tidak ada kesulitan harus mengoordinasikan ratusan orang sambil menjaga sistem tetap konsisten
    Biasanya satu orang menangani perubahan kode yang lebih teknis dan arsitektural, sementara satu orang lain menambahkan fitur business logic yang sangat besar untuk proses yang kompleks dengan cepat
    Jika alat AI kelas LLM saat ini atau masa dekat dipakai dengan hati-hati, menurut saya kita bisa mendapatkan sebagian efisiensi tim software yang sangat kecil namun sangat efektif
    Model yang terbayang bukan menghasilkan pembengkakan besar demi menghapus story point lalu menyerahkan masalah keberlanjutan ke orang lain, melainkan segelintir pemikir yang sangat tajam menjaga sistem tetap berada di jalur yang memberdayakan sekaligus manageable

  • Ini pedang bermata dua
    2 juta baris adalah pencapaian besar, tetapi sekaligus juga beban pemeliharaan yang signifikan
    Kelebihan Haskell jelas secara teoretis, tetapi kekurangannya lebih sulit ditangkap secara intuitif
    Godaannya adalah memodelkan segalanya lewat tipe
    Codebase itu sendiri akhirnya menjadi spesifikasi bisnis, bukan aplikasi
    Setiap perubahan kebijakan menjadi refactor besar, dan berkat keamanan Haskell, kadang hasilnya sangat padat karya secara mengejutkan
    Pada akhirnya Anda tidak bisa mendapatkan keduanya, dan suatu saat akan terjebak di dalam tipe
    Haskell sangat mengesankan dan kuat, terutama pada skala seperti ini, tetapi juga membawa masalah yang khas
    Godaan untuk memodelkan business logic dengan tipe bisa menciptakan struktur yang kaku, dan rasa aman dari struktur itu bisa membuat Anda tidak melihat jenis risiko lain

    • Kalau engineer berpengalaman dengan selera yang baik membangun bagian inti, mereka bisa meniti garis itu dengan cukup baik
      Anda memang tidak bisa mendapatkan semuanya, tetapi bisa mendapatkan banyak hal
      Beberapa tahun lalu saya magang di Jane Street, dan meski itu OCaml bukan Haskell, mereka tampak sangat baik dalam menyeimbangkan hal tersebut
      Meski bidangnya punya kompleksitas intrinsik tinggi dan reliabilitas serta akurasi langsung terkait kelangsungan bisnis, mereka tetap bergerak sangat cepat
      Kalau dipikir-pikir, inti Jane Street adalah mempekerjakan programmer OCaml berpengalaman dengan selera yang hebat seperti Stephen Weeks, lalu membiarkan mereka membangun library inti sejak awal dan memimpin seluruh codebase
      Sayangnya, Mercury tampaknya tidak seberhasil itu di bagian ini
    • TypeScript juga sama: https://www.richard-towers.com/2023/03/11/typescripting-the-...
      Sejujurnya, kelemahan terbesar dari tipe sistem yang Turing-complete adalah bahwa secara teori Anda bisa mengimplementasikan aplikasi yang akan berubah menjadi debu saat dikompilasi
  • Kisah sukses Haskell serupa dari Bellroy akan menjadi topik pertemuan Melbourne Compose yang akan datang: https://luma.com/uhdgct1v

  • Masalah saya dengan functional programming adalah debugging
    Lebih tepatnya, saya melihatnya sebagai salah satu kekuatan imperative programming, terutama gaya prosedural
    Dalam gaya fungsional/deklaratif, Anda biasanya menjelaskan keadaan seperti apa yang seharusnya ada, bukan bagaimana sesuatu dibuat, lalu bahasa tersebut menyusun semuanya dan memberi hasil akhirnya
    Kalau semuanya benar tentu bagus, mungkin bahkan lebih baik, tetapi kalau tidak dan hasil yang keluar tidak sesuai harapan, pertanyaannya jadi bagaimana menemukan bug-nya
    Dalam bahasa seperti C, ini relatif sederhana
    Anda telusuri baris demi baris, lihat keadaan eksekusi di antara setiap langkah, pada dasarnya RAM, dan jika berbeda dari harapan maka ada sesuatu yang salah di baris itu, jadi tinggal masuk ke sana dan lanjutkan dari situ
    Semakin bahasa mencoba menyembunyikan state seperti dalam functional programming, semakin sulit ini
    Menarik juga bahwa bagian terpanjang dari artikel itu adalah soal ini, yaitu “design for introspection”
    Penulisnya harus sengaja mengerahkan banyak upaya agar kode bisa di-debug, dan itu memberi wawasan bagus tentang penggunaan praktis Haskell yang sering diabaikan

    • Kiat debugging saya adalah membuat semua kode yang sedikit saja penting selalu mengembalikan output yang sama untuk input yang sama
      Bahkan kode sepele pun begitu
      Bahasa arus utama lain tidak mendekati ini
      Untuk situasi yang memang tidak bisa ditulis seperti itu, misalnya konkurensi shared memory, saya memakai transaksi
      Ini juga tidak bisa didekati oleh bahasa arus utama lain
      Belum lagi keuntungan mudah seperti tidak ada null, tidak ada integer casting implisit, dan sebagainya
      Bahwa debugging kode Haskell lebih sulit daripada bahasa lain itu sepenuhnya benar
      Tetapi kalau Anda menyingkirkan 90% sumber masalah kelas bawah, ya wajar saja hasilnya begitu
    • Debugging functional programming sering kali berbasis REPL, tidak seperti imperative programming
      Tentu ini bukan sesuatu yang unik untuk bahasa fungsional saja; bahkan di bahasa yang kebanyakan imperatif seperti Python atau JavaScript, orang juga sering memakai Python shell, browser console, shell Node/Deno/Bun, notebook, dan sebagainya sebagai lapisan debugging pertama
      Ada kompromi menarik dalam debugging yang berpusat pada REPL
      Dalam bahasa seperti C, Anda lebih sering mulai dari debugging seluruh program dan breakpoint, lalu mencoba menebak titik tepat yang kemungkinan bermasalah
      Di dunia yang berpusat pada REPL, Anda justru berusaha membuat komponen program lebih bisa diuji langsung dari REPL
      Karena itu batas modul/API/tipe jadi semakin mirip dengan kemampuan untuk di-debug
      Kadang ada tekanan yang lebih besar dibanding bahasa imperatif seperti C/C++ untuk membuat batas-batas ini dengan benar dan mudah dipakai
      Sebaliknya, dibanding debugging yang berfokus pada seluruh program, kadang jadi lebih sulit memisahkan masalah integrasi kompleks antar-unit dalam skenario nyata yang aneh
      Tetapi pendekatan REPL-first sering mendorong penyusutan luas permukaan integrasi hingga minimum, sehingga di bahasa fungsional efek integrasi seperti yang terlihat di bahasa imperatif bisa lebih jarang muncul
      Pernyataan bahwa bahasa fungsional menyembunyikan state rasanya kurang tepat
      Bahasa-bahasa ini juga berjalan di atas hardware imperatif dan tetap berurusan dengan state hardware yang nyata
      Di suatu titik memang ada penerjemahan antara dua dunia itu, tetapi mungkin tidak sedrastis yang dibayangkan
      Jika perlu, Anda tetap bisa kembali ke breakpoint imperatif dan debugger imperatif
      Karena itu saya menyebutnya debugging “berbasis REPL”
      Dengan REPL Anda bisa mempersempit unit yang bermasalah, yaitu modul/API/fungsi yang tepat beserta input yang menghasilkan output mengejutkan
      Jika bug-nya belum terlihat dari source saja, Anda bisa lanjut ke debugger imperatif untuk hampir pengalaman eksekusi baris demi baris yang sama, sambil mendapat konteks tambahan
      Pada titik itu, kemungkinan Anda sudah cukup mempersempit lewat REPL sehingga unitnya sendiri kecil dan sempit, jadi tidak terlalu perlu lagi memilih breakpoint yang bagus
      Menurut saya pesan yang diambil dari bagian “design for introspection” itu kurang tepat
      Bagian tersebut bukan tentang kemampuan debugging, melainkan tentang observability
      Isinya tentang memasang sistem logging/telemetri dengan benar, memalsukan dependency saat pengujian, dan menambahkan retry/circuit breaker di level keseluruhan sistem alih-alih menyerahkannya ke library masing-masing
      Di dunia imperatif pun ini bukan masalah debugging, melainkan masalah dekomposisi seperti dependency injection, pemasangan middleware, dan menggunakan abstract interface alih-alih concrete class di batas API publik
      Saran desain seperti ini adalah refactor, dan dampaknya lebih ke seberapa mudah Anda bisa memasang middleware observability pada API publik milik orang lain daripada ke kemampuan debugging
  • Sulit membayangkan sebenarnya 2 juta baris Haskell itu dipakai untuk apa
    Itu kode yang sangat banyak, padahal Haskell punya reputasi sebagai bahasa yang “padat” yang bisa melakukan banyak hal dengan sedikit kode
    Mungkin karena ada banyak library untuk JSON serialization/deserialization, framework REST API, logging, dan sebagainya

    • Menurut tulisan aslinya, masalahnya adalah kode yang tidak bisa diinstrumentasi tidak bisa dipercaya
      Jika binding pihak ketiga melakukan panggilan HTTP dengan fungsi konkret, Anda tidak punya cara menambahkan tracing, tidak punya cara menyuntikkan timeout sesuai SLO, tidak punya cara mensimulasikan kegagalan partner di pengujian, dan tidak punya cara menjelaskan celah 400ms dalam trace selain menebaknya secara teoretis
      Jadi mereka menulisnya sendiri
      Memang lebih banyak kerja di awal, tetapi client buatan sendiri itu sejak awal dibangun agar dapat diobservasi
    • Sifat yang Anda sebut “padat” itu biasanya disebut ekspresif tinggi
      Artinya ide yang relatif sangat abstrak bisa diungkapkan dengan sedikit karakter
      Sebagian orang juga menyebut ini “high-level”
      Tetapi menurut saya 2 juta baris bukan sebanyak yang terdengar pada awalnya
      Apalagi untuk perusahaan di domain yang sangat diatur seperti keuangan, dan code yang terkumpul selama bertahun-tahun
    • Ini sama sekali bukan metrik objektif, tetapi saya merasa Haskell hanya punya rasio aspek yang berbeda
      Jumlah barisnya mungkin agak lebih sedikit, tetapi jumlah katanya secara umum tetap mirip dengan bahasa OO yang lebih imperatif
    • Saya tidak tahu persis codebase-nya seperti apa, tetapi reputasi Haskell sebagai bahasa ringkas sebagian datang dari overrepresentasi dunia akademik atau teori kategori
      Di sana ekspresi seperti St M -> C T dianggap baik-baik saja, tetapi di software nyata jauh lebih berguna menulis TransactionState Debit -> Verified Transaction
      Bagian lainnya adalah faktor budaya yang jejaknya bisa ditelusuri sampai LISP
      Orang kadang terlalu sok pintar demi menghemat jumlah baris dengan trik atau macro yang sulit dipahami
      Di perusahaan keuangan seperti Mercury, saya kira yang didorong justru kejelasan dan keterbacaan, bukan gaya seperti itu
      Misalnya linter bisa saja memaksa pemecahan kode monadik menjadi ekspresi do yang teliti dan multi-baris, alih-alih menulisnya dalam satu baris dengan >> dan >>=