Jutaan Baris Haskell: Rekayasa Produksi di Mercury
(blog.haskell.org)- 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-sdksebagai 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, danvector, ada implementasi internal seperti alokasi yang bisa diubah, penulisan buffer, dan unsafe coercion - Monad
STmenggunakan perubahan in-place yang dapat diamati dan efek samping di dalam komputasi, tetapi tipe rank-2 darirunSTmencegah referensi mutable yang dibuat di dalamnya bocor keluarrunST :: (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
IOpada 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
- saldo tidak cukup harus berupa
- 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 terburukDynamic - 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
- tipe menjadi mendekati
-
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
- invariant yang mencegah kerusakan senyap lebih baik dimasukkan ke dalam tipe
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,
sendRequestbisa dibungkus dengan pengukuran waktu lalu mengembalikanHttpClientbaru - 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 -> Applicationmilik WAI, sangat berguna secara operasional
- Solusi yang paling sering dipakai adalah mengekspos record fungsi alih-alih fungsi konkret
-
Interceptor yang dapat dikomposisikan dengan
Monoid- Tipe middleware dan interceptor biasanya dapat memiliki instance
SemigroupdanMonoid Middlewarepada WAI adalah endomorphism, dan endomorphism membentuk monoid di bawah komposisi danid- Record hook interceptor bisa dikomposisikan per field, sehingga concern seperti tracing, timeout, dan rewrite task queue dapat digabung dengan
mconcattanpa plumbing terpisahappTemporalInterceptors = 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
- Tipe middleware dan interceptor biasanya dapat memiliki instance
-
Effect system
- Effect system seperti
effectful,polysemy,fused-effects, dancleffjuga 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
- Effect system seperti
-
Contoh positif dari
persistentSqlBackendmilikpersistentadalah record fungsi seperticonnPrepare,connInsertSql,connBegin,connCommit, danconnRollback- 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-clientsecara 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-apisebagai dependensi dan menaruh span di sekitar operasi intiIOsaja 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
stdoutataustderr, 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
- Alih-alih meng-import framework logging dan menulis langsung ke
- Mengekspos modul
.Internaljuga 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
.Internaldengan peringatan stabilitas yang eksplisit bisa lebih baik daripada pengguna harus mem-fork dan mem-vendor paket containers,text, danunordered-containersadalah 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
unsafePerformIOdigunakan di dalam library yang kita andalkan sehari-haribytestringdantextsecara 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
amountdi-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
- apakah field
- 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,
erroryang 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
Textalih-alihStringdalam 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
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
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)->EscapedAda 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-...
Misalnya di C# juga sangat mungkin dilakukan, tetapi noise visualnya jadi lebih besar daripada definisi tipe aslinya
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
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
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
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
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
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
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
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
Rasanya penulis ini akan tetap bisa menjalankan organisasi engineering yang sukses apa pun bahasa yang dipakai
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
cabal replSejujurnya 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
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
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
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
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
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
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
Jumlah barisnya mungkin agak lebih sedikit, tetapi jumlah katanya secara umum tetap mirip dengan bahasa OO yang lebih imperatif
Di sana ekspresi seperti
St M -> C Tdianggap baik-baik saja, tetapi di software nyata jauh lebih berguna menulisTransactionState Debit -> Verified TransactionBagian 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
doyang teliti dan multi-baris, alih-alih menulisnya dalam satu baris dengan>>dan>>=