Types sebagai Hansip: Validasikan Business Logic-mu saat Compile Time
Sekitar setahun yang lalu dari penulisan artikel ini, saya pernah menulis artikel tentang Nominal Typing (di Typescript) yang berguna untuk membedakan satu type dengan type lainnya walaupun struktur data keduanya identik. Dari sudut pandang lain, artikel tersebut juga meyebutkan bahwa Nominal Typing dapat digunakan untuk membatasi programmer dari kemungkinan melakukan kesalahan-kesalahan business domain sekaligus meningkatkan code expressiveness dengan memanfaatkan type system.
Artikel ini kurang lebih membahas konsep yang sama namun ditelaah menggunakan type system di Purescript.
Masalah dengan String dan Angka
Anggap ada requirement project yang melibatkan konsep mata uang. Katakanlah Rupiah dan Euro. Dan ada function addMoney
yang melakukan penjumlahan dua buah nominal uang.
Pemodelan fungsi seperti ini terlalu loose dan berbahaya karena tidak mampu mencegah programmer ketika melakukan penjumlahan dua buah mata uang yang berbeda. Kita gak mau kan secara naif menjumlahkan Rupiah dengan Euro karena Rp1 jelas gak sama dengan β¬1. Hal ini dapat menyebabkan komputasi yang tidak akurat dan bisa fatal kalau teledor π₯Ά
Salah satu cara mungkin bisa dengan menamakan kedua buah variable dengan penamaan yang jelas.
Ini pun masih error-prone dan tidak menyentuh akar masalah. Fungsi addMoney
masih bisa dipanggil dan gak bakal ngasih warning apa-apa ke programmer. Dengan kata lain kita masih memberikan ruang bagi data yang tidak valid untuk diproses. Big NO.
Hal yang terlihat sepele ini mengingatkan saya akan insiden tahun 1998 dimana NASA (hello flat-earthers!) merugi besar ketika kehilangan Martian Rover dalam ekspedisinya ke Mars hanya karena perbedaan metrics unit dalam kalkulasinya. Duh pak π
Pola yang sama juga bisa terjadi pada string. String has too much power. Ia mampu merepresentasikan apa saja yang bisa dibayangkan: karakter, kata, kalimat, nama, email, alamat, number (!!!), function, you name it.
Eh, function? Iya, function.
π»π»π»
Being powerful is great! Tapi perlu dicatat bahwa power yang sesungguhnya itu bukan yang digunakan tanpa batas, the real power comes when you can control it π
Bukanlah orang kuat yang sebenarnya dengan (selalu mengalahkan lawannya dalam) perkelahian, tetapi tidak lain orang kuat yang sebenarnya adalah yang mampu mengendalikan dirinya ketika marah.
β Muhammad SAW
Udah dong ceramahnya pak haji, balik ke programming.
Sama kayak programming, menurut saya sesuatu bisa dikatakan powerful ketika dapat membatasi programmer dari kemungkinan melakukan hal-hal gila. Dalam hal ini, batasan tersebut adalah type system.
Newtype
Di Purescript, newtype
bersifat opaque yang berarti walaupun secara struktur dan runtime representation-nya persis sama, mereka dianggap berbeda saat compile time.
Newtype sangat berguna untuk membuat distinction dari satu tipe data yang sama. Seperti ketika ingin membedakan konsep name
, address
, url
yang biasanya diekspresikan dengan string biasa.
Kembali ke masalah utama. Dengan teknik ini, kita bisa dengan mudah membedakan mata uang Rupiah dengan Euro!
Cuman masalahnya, bagaimana type signature dari fungsi addMoney
??? Kita ingin type signature addMoney
polymorphic terhadap berbagai jenis mata uang namu di saat yang bersamaan penambahan dua buah uang harus dilakukan dengan mata uang yang sama
Kind Signature
Untuk membuat fungsi addMoney
bekerja dengan Rupiah
dan Euro
, mereka harus dikelompokkan ke dalam sesuatu yang sama. Mari kita sebut Currency
.
Kita bisa lihat bahwa type variable a
tidak muncul di sebalah kanan persamaan, yang membuat type Currency
ini Phantom Type. Lalu gunanya a
ini apa?
Type variable a
berguna sebagai tag, sebagai pembeda antara satu mata uang dengan mata uang lainnya.
Sekarang type variable a
sudah diisi oleh type level string (Symbol): βeuroβ dan βrupiahβ. Kita perlu modifikasi definisi Currency sedikit karena, seperti yang kita tahu, type variable di Purescript by default memiliki kind Type
sedangkan type level string punya kind Symbol
.
Lalu batasi export data constructor Currency dan buat factory function sesuai kebutuhan project.
Dengan pattern seperti ini, data invalid yang dihasilkan dari penjumlahan nominal dua buah mata uang yang berbeda pun dapat dicegah:
Custom Kind
Pemodelan di atas masih belum bisa terlepas dari masalah string. Sekalipun ada di type level, pemodelan dengan string tetap rawan akan typo dan compiler tidak bisa menangkap kesalahan ini.
Kita harus mempersempit scope karena lagi, string can contain anything: dengan cara membuat kind kita sendiri.
Now the code looks safe already! πππ
Penutup
Ada beberapa hal penting yang bisa diambil di sini.
Yang pertama: having too much power is dangerous. Adanya type system yang berperan sebagai hansip selama ngoding dapat mencegah programmer dari kesalahan-kesalahan kecil nan fatal. Gak kebayang kalau masalah yang kelihatannya sepele ini bocor ke production dan merugikan banyak pihak.
Yang kedua, yang mungkin tidak terpikirkan sebelumnya: mengalihkan validation dari runtime ke compile time (dengan types) dapat menghemat banyak waktu debugging nantinya dan mengurangi penulisan test! Karena memang feedbacknya langsung terlihat: program compile atau tidak. Sedari awal kita tidak pernah membuat runtime validation secuilpun hanya untuk memastikan supaya Rupiah tidak tertukar dengan Euro. Namun code tetap safe dan hasil compile-nya pun gak kalah concise π
Type system is there for a reason. Anggapan-anggapan bahwa type system membuat programmer tidak bebas memang ada benarnya, tapi perlu dibarengi dengan kesadaran bahwa menjadi bebas tidak serta merta terbebas dari apapun. Ada konsekuensinya. Ada harga yang harus dibayar. Dan semua keputusan dalam programming adalah trade-off.