Bereksperimen dengan Row Types di Purescript
Belakangan saya lagi iseng-iseng main metaprogramming sama record system-nya Purescript, gimana cara nambahin value, ngubah type suatu attribute (key), menghilangkan attribute, dan lain-lain. Basic idea-nya sama kayak artikel saya yang lalu soal Row Polymorphism di Typescript. Tapi kali ini pake Purescript: gimana caranya operasi-operasi tersebut dapat ditangkap dan di-infer oleh compiler tanpa kehilangan type-safety.
Artikel ini gak akan berfokus pada akses record di Purescript, tapi lebih ke arah type-level programming untuk manipulasi record. Jadi contoh-contoh di bawah nanti gak akan saya sertakan implementation details-nya, hanya type signature-nya saja.
NOTE: Kalau temen-temen menemukan kata “attribute”, “label”, atau “key” di artikel ini, perlu saya garisbawahi bahwa saya merujuk pada hal yang sama :))
Symbol
Sebelum masuk ke pembahasan yang lebih lanjut, kita harus mengetahui terlebih dahulu apa maksud Symbol di Purescript. Symbol yang ini jangan disamakan dengan Symbol yang ada di Javascript. Symbol di Puresctipt digunakan untuk merepresentasikan type-level string.
Nah Proxy
dapat membantu kita membuat type-level string dan function reflectSymbol
untuk “menurunkan derajat” dari type-level string kembali ke term-level string.
Oh ya satu lagi, reflectSymbol
merupakan method dari class IsSymbol
sehingga harus digunakan sebagai constraint bila kita berniat memanggil reflectSymbol
.
Pada section berikutnya kita akan melihat kombinasi antara IsSymbol
dan Proxy
banyak digunakan untuk merujuk pada suatu attribute record di level type.
Cons
Sewaktu artikel ini ditulis (v13.3) di-update (v14.4), Purescript memiliki beberapa utility class untuk dapat memanipulasi dan menangkap informasi record saat compile-time. Salah satu yang paling sering digunakan adalah Cons
.
class Cons
mengekspresikan sebuah record yang memiliki attribute label
yang bertipe a
. tail
adalah sisa row
tanpa label
.
Perlu diperhatikan bahwa row bisa memiliki label yang duplikat di type level. Saya udah nanya soal kenapa row types bisa mengandung duplikasi label di slack channel tapi pak Harry sendiri juga gatau kenapa haha. Yo-uwes, mungkin kapan-kapan dijawab sama yang lebih berwenang.
Anyway, Cons
ini bisa diibaratkan seperti ini:
Mudah-mudahan cukup jelas bagaimana Cons
ini bekerja karena sebentar lagi kita akan melihat bagaimana kegunaan Cons
dalam meng-capture informasi type dari suatu record.
Manipulasi Record Type
Get
Mari kita latihan otak sekarang! Kita mulai dengan membuat type signature fungsi yang paling sederhana dulu, get
, yang kira-kira berfungsi seperti:
Bagaimana mengimplementasikan type signature fungsi tersebut di Purescript dan tetap row-polymorphic? Kita ingin nantinya fungsi get
di Purescript diakses mirip dengan fungsi Javascript di atas:
Ada beberapa yang langkah yang perlu diambil yang menurutku gak susah-susah amat asal udah ngerti konsep Symbol
dan Cons
. Langkah pertama adalah dengan mengimplementasikan apa yang bisa dan mudah diimplementasikan, walaupun belum sepenuhnya typecheck:
Dimana key
nantinya akan berupa type level string (Symbol) dan a
adalah type dari key yang diakses.
Langkah kedua, adalah dengan mengubah key
menjadi Proxy key
karena kita ingin string key yang di-supply dikenali oleh type checker.
We’re getting there! Sekarang kita harus membuat koneksi antara key
, row
, dan a
karena sebenarnya mereka adalah satu kesatuan yang tak terpisahkan: ada sebuah row
dengan attribute key
yang bertipe a
. Bagaimana cara mengekspresikan relasi ini?
Dengan Cons
! Ingat bahwa Cons
digunakan untuk mengekspresikan sebuah record yang memiliki suatu attribute tertentu.
Dan terakhir, dengan asumsi kita memiliki sebuah FFI sebagai implementation details untuk dieksekusi saat runtime, kita harus menambahkan constraint IsSymbol
agar type-level key di parameter pertama dapat direalisasikan sebagai string biasa.
dimana getImpl
sendiri adalah
Selesai! 🎉 Abaikan saja dulu tail
di sini dan jangan terlalu dipikirkan, nanti akan ada saatnya kita menggunakan tail
. Sekarang kita lihat dulu apakah compiler dapat meng-infer type yang tepat dengan type signature ini..
Nice work, brain 🧠!
Set
Jom naik level: mari membuat fungsi set
yang memungkinkan kita untuk mengubah nilai pada suatu attribute sekaligus dapat mengubah type-nya.
Komputasi di atas mengubah type “name” yang semula bertipe String
menjadi Array String
. Berarti akan ada duah buah row yang berbeda yang harus dimasukkan ke dalam type signature: row dengan “name” bertipe String
, dan row dengan “name” bertipe Array String
. Namun sebelumnya, lakukan upacara dengan Symbol dan kawan-kawannya agar mempermudah langkah selanjutnya.
Lalu asosiasikan key
dan b
dengan rowB
menggunakan teman kita Cons
karena mereka satu kesatuan republik indonesa:
Lagi, abaikan tail
untuk saat ini. Sekarang mari kita pikirkan sejenak relasi rowA
dengan rowB
. Mereka sebenarnya adalah row
yang sama, hanya type dari key
-nya saja yang kemungkinan berbeda. Karena masih ada relasi satu sama lain, kita harus melakukan penggabungan dua buah record ini dengan Cons
:
Dengan penambahan constraint ini, typechecker dapat melihat relasi antara keduanya dengan benar: bahwa keduanya memiliki key
dan tail
yang sama, hanya type dari key
-nya saja yang berbeda.
Nah, markicek apa sudah benar implementasi type signature di atas dengan menggunakan fitur type hole.
Typechecked! ✅
Delete
Fungsi delete
menghapus sebuah attribute dari suatu record dan mengembalikan row baru tanpa attribute tersebut. Kita ingin fungsi ini dipanggil seperti:
Lagi, langkah pertama dalam menulis type signature yang dirasa agak kompleks adalah dengan menuliskan apa yang mudah ditulis.
rowA
adalah record yang attribute key
-nya ingin dihapus, menghasilkan rowB
. Dengan kata lain, rowB = rowA - key
. PR kita tinggal mengekspresikan relasi ini ke dalam type signature. Dan saya rasa Cons
masih bisa menjadi jawaban atas problem ini.
Kita review ulang dulu struktur class Cons
biar freshhhh.
Kita dapat melihat pola bahwa tail adalah row tanpa head. Persepsi ini seolah memberikan kesimpulan bahwa rowB
adalah tail dari rowA
.
Masih ada relasi yang kelewatan? Kalo gak ada langsung aja kita buktikan apakah type signature di atas typechecked..
Typechecked!
Tapi belum sepenuhnya benar. Ingat bagaimana row bisa menampung label yang duplikat? Yes, kita masih harus benar-benar meyakinkan compiler bahwa rowB
tidak memiliki label key
. Caranya dengan memberikan constraint Lacks
.
Purescript memiliki class bernama Lacks
yang bisa digunakan untuk mengekspresikan suatu record yang tidak memiliki attribute tertentu.
Yang bisa dibaca dengan: “Hey compiler, tolong assert bahwa record (name :: String, age :: Int)
tidak memiliki attribute/label bernama nonExistingKey
”. Karena tidak ditemukan maka expression di atas typechecked. Sebaliknya, compiler akan menolak untuk memberikan lampu hijau jika ditemukan Symbol pada record yang di-assert.
Oleh karena itu type signature fungsi delete
masih bisa di-improve lagi dengan memberikan constraint Lacks
:
Insert
Fungsi insert
juga dapat dibuat dengan mengkombinasikan class Lacks
dengan Cons
. Intuisi type signature fungsi insert
ini saya serahkan ke pembaca untuk exercise 🙂
Wrap Up
Purescript secara default menyediakan beberapa class dan data structure yang dapat digunakan untuk memanipulasi informasi rows di type level. Umumnya mereka bisa ditemukan di module Prim.Row
dan Prim.RowList
.
Kalo temen-temen mau lihat fungsi-fungsi lain untuk record manipulation sebagai inspirasi, mungkin bisa mampir ke purescript-record. Dokumentasi dan Doc Comment-nya cukup jelas dan mudah diikuti.
Saya harap pembahasan di artikel ini gampang dicerna dan gak terlalu kompleks. Dan yang paling penting, semoga masih bisa memberi bermanfaat. Sayonara ✌🏻