Type Class Dan Cara Kerjanya Di Balik Layar

Aug 23, 2019 01:40 · 930 words · 5 minute read #purescript #types #typeclass

Type Class Dan Cara Kerjanya Di Balik Layar
Image by Jarkko Mänty from Pixabay

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.

TypeScript
interface Size {
  len: () => number;
  whoAreYou: () => string;
}

class MyNumber implements Size {
  constructor(private num: number) {}

  len = () => this.num
  whoAreYou = () => "aku number"
}

class MyString implements Size {
  constructor(private str: string) {}

  len = () => this.str.length
  whoAreYou = () => "aku string"
}

Daaan kita bisa buat sebuah function yang polymorphic terhadap segala jenis Size:

TypeScript
const lengthPlusTen = (size: Size) => size.len() + 10

const res1 = lengthPlusTen(new MyNumber(5))         // 15
const res2 = lengthPlusTen(new MyString('waspada')) // 17

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.

PureScript
class Size s where
  len :: s -> Int
  whoAreYou :: s -> String

instance sizeInt :: Size Int where
  len n = n
  whoAreYou _ = "aku integer"

instance sizeStr :: Size String where
  len = Data.String.length
  whoAreYou _ = "aku string"

lengthPlusTen ::  s. Size s => s -> Int
lengthPlusTen s = (len s) + 10

res1 = lengthPlusTen 5         -- 15
res2 = lengthPlusTen "waspada" -- 17

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.

PureScript
type SizeDict s = {
  len :: s -> Int,
  whoAreYou :: s -> String
}

Juga untuk tiap-tiap “instance”-nya. Saya ambil yang integer saja dulu.

PureScript
sizeIntDict :: SizeDict Int
sizeIntDict = {
  len: \n -> n,
  whoAreYou: \_ -> "aku integer"
}

Pun function len yang semula memiliki type signature ∀ s. Size s => s -> Int juga mengalami proses desugaring ini.

PureScript
-- semula
len     ::  s. Size     s => s -> Int

-- menjadi
lenDict ::  s. SizeDict s -> s -> Int
lenDict dict s = dict.len s

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:

PureScript
-- semula
res1 = len 5
-- menjadi
res1 = lenDict ? 5

Somehow kita membutuhkan sizeIntDict untuk mengisi placeholder ?. Dan kita hanya bisa mengandalkan compiler untuk melakukan type inference pada fungsi lenDict yang mana (dalam konteks ini) pasti memiliki type signature:

PureScript
lenDict :: SizeDict Int -> Int -> Int

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:

PureScript
res1 = lenDict sizeIntDict 5

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.

JavaScript
// class Size s ...
var Size = function (len, whoAreyou) {
  this.len = len;
  this.whoAreyou = whoAreyou;
};

var len = function (dict) { // `lenDict`
  return dict.len;
};
var whoAreyou = function (dict) {
  return dict.whoAreyou;
};

// instance sizeInt :: Size Int ...
var sizeInt = new Size( // `sizeIntDict`
  function (n) {
    return n;
  },
  function (_) {
    return "aku integer";
  }
);

// instance sizeStr :: Size String ...
var sizeStr = new Size(
  Data_String.length,
  function (_) {
    return "aku string";
  }
);

var lengthPlusTen = function (dictSize) {
  return function (s) {
    return len(dictSize)(s) + 10 | 0;
  };
};
var res1 = lengthPlusTen(sizeInt)(5);
                         ^^^^^^^
//                  dictionary integer

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.

PureScript
res2 = len "waspada"
JavaScript
var res2 = len(sizeStr)("waspada");
               ^^^^^^^
//        dictionary string

Sekian dari saya kurang lebihnya diambil aja 🙏🏻

Edit on