Kenalan Dulu sama Type Class
Tidak semua bahasa pemrograman mendukung fitur type class. Beberapa orang bilang konsep type class identik dengan konsep interface pada bahasa-bahasa seperti Java, C#, atau Typescript, dimana type class dan interface sama-sama bertujuan untuk menyediakan function yang polymorphic, walaupun sebenarnya keduanya tidak 100% sama. Identik saja.
Saya gunakan Purescript untuk menjelaskan konsep type class pada article ini.
Give Me the Code
- Kita membuat sebuah type class dengan nama
Show
. Umumnya type class menyediakan satu (atau lebih) type variable yang bakal digunakan di method-nya. Di sini type variable tersebut adalaha
. - Type class
Show
ini memiliki satu method bernamashow
, yang menerimaa
dan mengembalikan String.
Tidak ada concrete code di sini, tugas type class hanya mendefinisikan struktur (type signature) dari method-method yang bisa digunakan oleh suatu tipe data. Seperti interface, hanya kontrak. Detil implementasi diserahkan ke implementornya, seperti tipe Email di bawah:
- Di sini kita membuat instance
Show
untukEmail
dengan namashowEmail
(nama instance bisa kita abaikan, tidak terlalu penting untuk saat ini). - Implementasi
show
. Inilah gunanya type variablea
di atas tadi. Sekaranga
telah di-instantiate denganEmail
sehingga bisa kita konsumsi di function argument.
Karena perannya yang mirip dengan interface, kita juga bisa membuat instance untuk tipe data lain. Sehingga function show
tidak hanya applicable untuk type Email, tapi juga Password.
Kalau type class identik dengan interface, lalu dimana bedanya?
Type Class vs Interface
Walaupun type class sekilas terlihat seperti interface, ada perbedaan yang sangat mendasar, yaitu type class memungkinkan kita untuk membuat instance terhadap type yang bukan milik kita. Oleh karenanya banyak orang yang menyebut type class sebagai “the true ad-hoc polymorphism”.
Tak perlu jauh-jauh memikirkan tipe data dari third-party library, kita bahkan bisa memberi instance untuk tipe data primitif seperti Boolean.
Tipe data primitif lainnya seperti Int, String, Array sudah “diurus” oleh Prelude. Semua di level library, no magic.
Appendable
Let’s dig deeper.
Ambil String dan Array. Keduanya memiliki sifat yang sama yaitu dapat digabungkan; string dengan string, array dengan array. Di Javascript, penggabungan ini bisa dicapai dengan menggunakan operator +
.
Behavior atau sifat “bisa digabungkan” ini bisa kita capture dengan sebuah type class, sebut saja Appendable
.
Kita baru saja memberikan String dan Array kemampuan untuk bisa digabungkan dengan membuat instance Appendable
. Detil implementasinya kita biarkan kosong agar contoh code-nya tetap sederhana. Bila kita panggil fungsi append
dengan tipe data yang belum memiliki instance Appendable
seperti Int, program akan error.
INTERMEZZO
📝 Di Typescript, kemampuan ini “bisa” dicapai dengan memanfaatkan fitur augmentation dan JS prototypes!
Subtyping
Sama seperti interface, type class juga memiliki konsep subtyping dimana sebuah type class dapat “meng-extend” method dari type class lainnya.
Type class HazDefault
di-construct dengan superclass Appendable
, yang memungkinkan instance HazDefault
untuk tetap bisa memanggil fungsi append
, dengan syarat setiap instance HazDefault
harus juga memiliki instance Appendable
. Relasi sebuah type class dengan superclass-nya ditandai dengan symbol <=
.
String dan Array sudah memiliki instance Appendable
, maka sah-sah saja bagi mereka untuk juga memiliki instance HazDefault
😉
Compiler akan complain dengan pesan yang cukup jelas kalo kita iseng membuat instance HazDefault
untuk tipe data yang belum memiliki instance Appendable
.
Overlapping Instances
Sejauh ini kita sudah belajar bagaimana type class berguna untuk memberikan “kemampuan” pada suatu tipe data. Kita sudah memberi String dan Array “kemampuan” untuk melakukan penggabungan dengan fungsi append
dan mengembalikan nilai kosongnya dengan fungsi defaultVal
.
Apa yang terjadi jika kita memberikan kemampuan yang sama dua kali?
Compiler komplain. Masuk akal sih, karena nanti ketika ada code defaultVal :: String
compiler akan kebingungan memilih harus pakai instance yang mana: apakah harus mengembalikan ""
atau "zzz"
. Kondisi ini disebut Overlapping Instances. Namun jika tetep kekeuh ingin menuliskan overlapping instances, Purescript menyediakan fitur Instance Chains yang tidak akan saya bahas di artikel ini.
Tapi kadangkala ada saja kasus dimana suatu data bisa memiliki dua behavior: misal untuk tipe Int
jika mengimplementasi class HazDefault
. Nilai default Integer bernilai 0 ketika dijalankan dalam konteks penjumlahan, namun bernilai 1 dalam konteks perkalian. Ketika dihadapkan dengan situasi seperti ini, salah satu cara untuk mengakalinya bisa dengan membungkusnya dengan newtype
.
Constraint
Type class is all about instance and constraint. Mari perhatikan contoh berikut:
Constraint type class pada sebuah function dipisahkan dengan symbol =>
. Fungsi guard
menerima dua buah argument; Boolean
dan a
, dan mengembalikan a
. Namun a
di sini tidak boleh sembarang type, ia harus mempunyai instance HazDefault
.
Kita sudah tahu bahwa fungsi guard
ter-contraint dengan class HazDefault
pada type parameter a
, yang berarti a
hanya boleh diisi dengan String atau Array.
In fact, kalau kita menginspeksi type append
dan defaultVal
di REPL, yang kita lihat sebenarnya adalah:
Tidak mengejutkan 🙂
Methods Injection
Selain kemampuan membatasi akses pada suatu function, constraint type class pada dasarnya memberikan semua method-nya secara implisit. Untuk lebih jelasnya, mari modifikasi function guard
barusan.
Dengan adanya constraint HazDefault
di type signature, function guard
memiliki izin untuk mengakses method-method yang ada pada class HazDefault
(append
dan defaultVal
). Under the hood, compiler melakukan proses desugaring seperti berikut.
Dengan kata lain, function append
dan defaultVal
bersifat eksklusif, tidak bisa sembarang dipanggil oleh function lain. Caller harus memberikan constraint di type signature-nya. Jika tidak, compiler akan complain.
Readability dan Testability
Bicara agak real world, type class juga dapat membantu readability ketika kita ingin melacak side effect apa saja yang bakal dijalankan di sebuah function.
Fungsi fetcUser
ter-constraint dengan dua buah type class: MonadCache
dan MonadUserDb
, yang memungkinkan untuk memanggil fungsi readCache
, writeCache
, dan getUser
di dalamnya. Fungsi fetchUser
juga secara tidak langsung ter-constraint dengan type class Monad
sehingga kita bisa langsung menggunakan do
binding.
Gak hanya itu, kita juga bisa menyimpulkan dari melihat type signature-nya saja bahwa fetchUser
bakal berinteraksi dengan cache dan database (side effect) tanpa terikat dengan implementasi apapun. Implementasi tergantung konteks caller-nya. Misal ketika testing, instance bisa dibuat semau kita.
Penutup
Penggunaan type class ada dimana-dimana. Eq
, Show
, Functor
, Monad
, Applicative
, Semigroup
(Appendable), Monoid
(HazDefault), dan Traversable
adalah beberapa type class dasar yang wajib dipahami.
Dari type class jugalah kita sebenarnya bisa melihat bahwa pattern dalam programming dapat di-abstraksi sejauh mungkin. Appendable (biasa disebut Semigroup
) dan HazDefault (biasa disebut Monoid
) hanyalah contoh kecil saja. Video di bawah ini gak pernah bosen saya rekomendasikan untuk melihat bagaimana cara mengeneralisasi pattern dari sebuah masalah yang berujung pada terbentuknya type class.
Semoga bermanfaat 🙂