Membantu Compiler dengan Functional Dependencies
Udah cukup banyak artikel yang menjelaskan tentang apa itu Functional Dependencies — salah satu fitur type system di Haskell dan Purescript — beserta use cases-nya. Namun yang tak rasakan justru kurang mudah dipahami dan sulit dianalogikan dengan studi kasus lain. Liat aja contoh dari Haskell Wiki sendiri, FuncDep dijelaskan dengan studi kasus Vector dan Matriks yang Vector sendiri aja gue gak paham 🤣 Nah, artikel ini murni tak coba tulis untuk mematangkan pemahaman saya pribadi sekaligus berharap ada masukan dan kritik dari teman-teman yang lebih paham.
Multi-parameter Type Classes
Lahirnya fitur Functional Dependencies ini katanya terinsipirasi dari fitur Functional Dependencies yang ada pada relational database, yaitu relasi attribute antar table (biasanya berurusan dengan Primary Key). Ada One to One, One to Many, Many to One, dan Many to Many. Relasi dalam database ini dan korelasinya dengan FuncDep akan di bahas di bawah. Sekarang mari bahas Multi-param type classes dulu.
Functional Dependencies baru dapat digunakan ketika kita menulis class yang memiliki type parameter lebih dari satu, alias multi-parameter type class.
”Baru dapat digunakan” dalam arti kita bisa memilih untuk menggunakan fitur FuncDep atau tidak, tergantung kasus-nya. Jika tidak menggunakan FuncDep, maka a
dan b
dapat diisi dengan type apa saja tanpa ada restriction, seperti relasi many-to-many pada database. Any combination of a
and b
is valid. Akan berbeda ketika FuncDep digunakan:
Di sini compiler bilang: “gue bakal bisa infer type b
asal gue tau dulu apa type a
”. Dengan kata lain a
uniquely determines b
.
Setidaknya itu yang banyak tak baca di internet, yang awalnya justru membuat saya merasa bersalah karena tau artinya secara literal tapi tidak secara kontekstual.
Many to One
Nah seperti yang sudah dijelaskan di atas (bahwa FuncDep terinspirasi dari database), maksud dari notasi a -> b
dapat dipahami menggunakan intuisi yang sama ketika memahami relasi many-to-one pada relational database.
Misal ketika bicara Geografi, many-to-one bisa dianalogikan dengan relasi kota terhadap provinsi: satu kota hanya ada pada satu provinsi, tapi satu provinsi dapat memiliki banyak kota.
Kalau saya tanya ke kamu: “Surabaya ada di Provinsi apa?” Pasti jawabannya cuman satu dan memang satu-satunya jawaban, Jawa Timur. Tapi sebaliknya kalau ditanya: “Jawa Timur kotanya apa?”, kita gak akan bisa jawab hanya dengan satu kota saja karena jawabannya banyak, akan ada Surabaya, Malang, Jember, Banyuwangi, dan kota-kota lainnya.
Maka betul kata compiler tadi: “gue bakal bisa infer type province
asal gue tau dulu apa type city
”. Tapi darimana compiler bisa tau? 🤔
Dari instance yang kita tulis 👇🏻
Jadi sebenarnya tidak ada magic di sini: kita yang mendikte compiler.
Bagaimana kalau saya, misal, mau bilang Surabaya itu juga bagian dari Jawa Tengah? Mungkin pipi saya bakal jadi merah digampar sama Pak Guru. Alasannya? IQ lu jongkok! 🙊
Dari sini bisa ditarik kesimpulan, bahwa functional dependencies berguna untuk membatasi jumlah instance type variable yang muncul di sebelah kiri arrow (a
pada a -> b
atau city
pada city -> province
). Dengan kata lain type variable di sebelah kiri arrow harus bersifat unique, tidak boleh muncul di class instance lebih dari satu kali. Tanpa FuncDep, code di atas akan dianggap valid oleh compiler.
Kasus yang paling umum di dunia nyata adalah ketika menggunakan MTL MonadThrow
yang menspesifikasikan bahwa setiap Monad yang implements class MonadThrow
harus memiliki satu error type saja, tidak boleh lebih.
Dengan begini, compiler juga dapat langsung mengetahui apa type e
begitu type m
diketahui. Artinya, jika nanti ada function yang menggunakan Aff
monad dan memanggil throwError
di dalamnya, compiler bisa langsung tahu bahwa e
pastilah bertipe Error
dan bukan yang lain. Demikian pula ketika ada function di dalam konteks TestM
, begitu ada pemanggilan throwError
compiler akan langsug bisa meng-infer e
sebagai String
.
One to One
Fitur Functional Dependencies juga bisa memiliki spesifikasi yang lebih narrow dari relasi many-to-one, yaitu one-to-one.
Dengan one-to-one, kita menjamin jumlah instance type variable a
dan b
hanya satu saja. Mereka tidak boleh muncul lebih dari satu kali. Masih berkaitan dengan geografi, contoh yang paling mudah adalah relasi antara ibu kota dengan negaranya: suatu negara hanya boleh memiliki satu ibu kota dan suatu ibu kota hanya boleh dimiliki oleh satu negara.
Ketika ada pertanyaan: “Apa ibu kota negara +62?“. Jawabannya pasti hanya satu yaitu Jakarta. Dan ketika ditanya balik: “Jakarta ibu kota negara apa?“. Jawabannya juga hanya satu yaitu negara +62.
Dengan relasi seperti ini, compiler bisa menginfer salah satu type asal type yang satunya sudah diketahui. Relasi one-to-one ini juga bisa disebut Bidirectional Dependencies.
Extra Type Variable
Section ini masih berkaitan dengan many-to-one relationship, yang membedakan hanyalah jumlah type parameter di sebelah kiri atau kanan arrow. Kalau sebelumnya hanya satu (seperti a -> b
), yang ini bisa lebih dari satu (seperti a b -> c
). Namun tetap tidak mengubah arti: jika compiler tahu apa type a
dan b
, maka type c
otomatis dapat langsung diketahui.
Anggap kita ingin mengimplementasikan behaviour +
di Javascript yang dapat menerima lebih dari satu type: Float (Number
di Purescript), dan String. Yang secara umum dapat direpresentasikan sebagai berikut:
+ | Number | String |
---|---|---|
Number | Number | String |
String | String | String |
Dengan aturan ini, kita dapat melihat setidaknya ada dua buah pola menarik:
- Kombinasi type kedua buah operand (in bold) bersifat unique. Tidak ada kombinasi yang muncul lebih dari sekali
- Type hasil penjumlahan dengan operator
+
ditentukan oleh kedua buah type operand
Code-wise, aturan tersebut dapat dituliskan dengan sebuah class yang menerima tiga buah type variable.
Sekarang malah keliatan kayak pattern-matching tapi di level type 😅 “Kalo aku punya String dan Number, maka hasilnya harus String” dan seterusnya. Tapi yang pasti, function jplus
ini mengembalikan return type yang berbeda-beda tergantung “input”-nya.
Kasus Functional Dependencies dengan type parameter lebih dari dua ini bisa ditemukan ketika bermain dengan Record. Seperti type Union yang memiliki type signature:
Yang pada dasarnya hanya melakukan penggabungan 2 buah row dan menghasilkan sebuah row baru hasil penggabungan tersebut. Intuisi selanjutnya di balik type Union ini saya kembalikan ke masing-masing pembaca.
Function Overloading
Dari contoh di atas FuncDep sekilas terlihat seperti function overloading! Dan memang benar, FuncDep “bisa” digunakan untuk meng-encode function overloading seperti yang lumrah ada pada bahasa pemrograman lain. Berikut perbandingan yang identik di Typescript:
Yang membedakan, function overloading ini tidak memiliki relasi antar type seperti yang ada pada FuncDep. Program di bawah ini typecheck, walaupun jika dijalankan, overload yang terakhir tidak akan pernah dipanggil.
Sedangkan compiler Purescript sendiri akan menolak fungsi di atas (jika menggunakan FuncDep) karena kombinasi type x
dan y
overlap (tidak unique).
Namun Functional Depndencies lebih dari sekedar function overloading. Contoh yang real worldish adalah ketika mencoba re-implement State Monad dan membuat instance dengan Ref (mutable variables).
Bagian m -> r, r -> m
mengindikasikan adanya dua buah FuncDeps (Bidirectional Dependencies) sekaligus mengekspresikan relasi one-to-one. Sekarang perhatikan code berikut:
Adanya pemanggilan fungsi log
(yang memiliki type String -> Effect Unit
) berimplikasi pada asumsi bahwa fungsi someFn
ada di dalam Effect
monad, yang, kalau dilihat dari Functional Dependencies-nya, type r
pasti merujuk pada Ref.Ref
. Dan pada akhirnya, compiler akan dengan sendirinya meng-infer fungsi someFn
sebagai
Andaikan FuncDep tidak digunakan, fungsi someFn
akan memiliki type
dimana r
tidak dapat di-infer oleh compiler. Di lain kasus, tidak adanya FuncDep dapat menimbulkan ambiguity di sisi compiler.
Kesimpulan
Functional Dependencies bisa digunakan oleh programmer ketika ingin memberikan constraint terhadap type saat proses type inference dengan mendeklarasikan relasi di multi-param type class (one-to-one, many-to-one). Dengan FuncDep compiler dapat didikte/dibantu untuk mengetahui type mana yang bisa langsung di-infer dari type lain. Yaa itung-itung amal baik ke compiler yang selama ini sudah banyak ngebantu report error sana sini 😁
Mudah-mudahan artikel ini dapat membantu memahami motivasi dan kegunaan dari Functional Dependencies. Ciao 👋🏻