Type Class Dan Cara Kerjanya Di Balik Layar
Buat kamu-kamu yang punya background di OOP dan udah kenalan sama konsep interface, pasti enak banget kan bisa bikin aplikasi yang polymorphic dan gak terikat sama specific implementation. Kalau mau nambah variant, tinggal bikin class baru yang implement interface tersebut, selesai deh gak perlu ubah-ubah bagian code yang lain.
Klo interface di FP gimana, pak? Tergantung bahasanya, karena di FP ada bahasa yang typed dan untyped. Berhubung konsep interface ini ada di bahasa yang typed dan saya lagi explore Purescript sebagai bahasa FP yang typed, bolehlah saya bantu jawab sedikit bahwasanya konsep interface ini bisa disandarkan dengan “type class”. Class di Purescript nggak sama kayak class di bahasa OOP pada umumnya, class di PS justru lebih kayak interface. More on this later.
Saya ambil contoh penggunaan interface di Typescript.
Daaan kita bisa buat sebuah function yang polymorphic terhadap segala jenis Size
:
Fungsi lengthPlusTen
jelas dapat mengakses method len()
karena variable size
merupakan instance dari Size
yang memiliki method len()
. Sekarang bandingkan dengan “interface” yang ada di Purescript.
Di sini kita nggak mengirim instance apa-apa seperti yang kita lakukan di Typescript, alih-alih hanya mengirimkan plain integer (5
) dan plain string ("waspada"
). Namun compiler automagically bisa dengan benar memilih instance mana yang harus digunakan untuk masing-masing data.
Muncul pertanyaan: sebenernya gimana sih cara kerja “class” ini di balik layar sehingga compiler bisa tahu mana instance yang harus diambil?
Dictionary
Saya sendiri sebenarnya belum tahu secara persis bagaimana proses class resolution dilakukan di Purescript. Namun saya menemukan artikel menarik yang menjelaskan bagaimana Haskell menggunakan teknik dictionary passing agar compiler dapat menentukan dan memilih instance yang benar.
Saya pun double-check di Slack Channel #purescript apakah proses tersebut juga dilakukan oleh compiler Purescript, Pak Harry menjawab iya. Mantaplah qlo beqitu 🎉🎉🎉. Dengan catatan, hanya rough idea aja ya 🙂
Jadi gini bapak-bapak ibu-ibu, setiap class yang kita buat, compiler juga membuat representasinya sendiri dengan sesuatu yang disebut “dictionary”. Misal untuk contoh kasus class Size n
tadi, compiler akan melakukan proses desugaring untuk class tersebut dan membuat dictionary-nya dengan record.
Juga untuk tiap-tiap “instance”-nya. Saya ambil yang integer saja dulu.
Pun function len
yang semula memiliki type signature ∀ s. Size s => s -> Int
juga mengalami proses desugaring ini.
Eaa sekarang malah mirip kayak contoh Typescript tadi!
Dengan proses desugaring seperti ini, ketika saya menyuplai fungsi len
dengan angka 5
, maka sebenarnya code saya akan “diubah” menjadi:
Somehow kita membutuhkan sizeIntDict
untuk mengisi placeholder ?dict
. Dan kita hanya bisa mengandalkan compiler untuk melakukan type inference pada fungsi lenDict
yang mana (dalam konteks ini) pasti memiliki type signature:
Setelah monotype diketahui (Int
), compiler gak menemukan kandidat yang type signature-nya cocok dengan SizeDict Int
selain variable sizeIntDict
. Akhirnya compiler pun menyuplai dictionary ini ke fungsi lenDict
:
Dan selesailah proses “pencarian instance” ini dengan teknik dictionary passing. Seluruh proses ini memberikan kesimpulan bahwa type class adalah sebuah cara untuk memberikan instance dictionary secara implisit.
Compile ke Javascript
”Sisa-sisa” proses desugaring dengan dictionary ini bisa dilihat ketika code di atas di-compile ke Javascript.
Dari output ini terlihat jelas bahwa fungsi len
, whoAreYou
, dan fungsi lain yang depend on them seperti lengthPlusTen
membutuhkan “dictionary” saat runtime. Dictionary tersebut di-resolve oleh compiler saat compile time bergantung pada konteksnya. Artinya kalo kita kasih string ke dalam fungsi len
, dictionary yang di-resolve oleh compiler pun pasti akan berbeda.
Sekian dari saya kurang lebihnya diambil aja 🙏🏻