Code Reuse — Berkaca dari Functor
Ada banyak cara untuk memahami Functor. Yang paling umum dan banyak dijumpai ialah dengan penjelasan wrap-unwrap data, seperti di artikel Bungkus Kacang atau ilustrasinya mas Adit. Di artikel ini saya mencoba menjelaskannya dari sudut pandang lain: code reuse.
Motivasi
Mari kita mulai dengan sesuatu yang simple: sebuah angka. Lalu kita lakukan transformasi pada angka tersebut dengan fungsi incr
dan decr
.
Easy. Namun apa jadinya jika angka tersebut dimasukkan atau dibungkus ke dalam suatu data structure seperti Array? Apa ia masih bisa di-increment atau decrement?
Ekspektasi kita adalah ketika incrArray
dijalankan, angka yang ada di dalam Array harus bertambah satu. incrArray [5]
harus menghasilkan [6]
. Demikian pula dengan decrArray
ketika dijalankan, angka yang ada di dalam Array harus berkurang satu. decrArray [10]
harus mengembalikan [9]
.
Kalo maunya begitu, apa fungsi incr
dan decr
masih bisa digunakan?
Banyak pertanyaan untuk dijawab 😄
Reusability
Sebagai seorang programmer kita terbiasa melihat pola dalam code dan membuat abstraksinya agar code tidak redundan. Tentunya kita tidak ingin membuat fungsionalitas increment-decrement dari awal lagi hanya untuk membuatnya bekerja dengan Array. Kita ingin incr
dan decr
reusable.
Salah satu upaya agar code menjadi lebih reusable adalah dengan memanfaatkan Higher-Order Function.
Fokus kita ada di type signature: transformArray
menerima fungsi (Int -> Int)
di argument pertamanya, cocok dengan incr
dan decr
yang juga bertipe Int -> Int
! Bisa langsung disubstitusi deh.
Walaupun incr
dan decr
berhasil di-reuse, transformArray
ini belum cukup reusable: ia hanya menerima fungsi bertipe Int -> Int
. Fungsi dengan type signature lain seperti isEven :: Int -> Boolean
atau showInt :: Int -> String
tidak bisa digunakan.
Lebih parahnya lagi, fungsi transformArray
hanya bisa memproses Array Int
! Sedangkan tau sendiri, Array itu termasuk struktur data yang generic, bisa terisi oleh berbagai macam tipe data: Array String
, Array Boolean
, Array User
dan lain sebagainya. Kita ingin fungsi ini lebih general dan reusable.
Di sinilah generic mulai berperan. Kita harus mengubah type signature yang terlalu spesifik terhadap Int dengan generics.
Perubahan dari concrete type ke generics memperluas lingkup function, menjadikannya jauh lebih reusable. Lihat saja contoh-contoh berikut, kita tak lagi terikat dengan Int -> Int
dan bisa bekerja dengan berbagai macam Array 🎉🎉🎉
Bentuk Lain
Kita sudah berhasil “menakhlukkan” Array. Saatnya membahas struktur data generic lain yang agak menantang. Let’s talk about Tree.
Tree sama seperti Array, ia bisa menampung berbagai macam jenis data: Tree Int
, Tree String
, Tree Boolean
, dll. Bisa ditransformasi juga? Harusnya sih bisa. Karena kita sudah belajar bagaimana membuat fungsi yang reusable pada Array, kita pun juga ingin agar fungsi transformasi pada struktur data Tree ini reusable.
Fokus pada type signature, kita melihat pola yang sangat identik pada fungsi transformTree
dan transformArray
. Mereka identik dalam 2 hal:
- Behaviour (sama-sama sebagai fungsi transformasi)
- Type signature
Mungkin ketika suatu saat nanti ada struktur data lain (misal Queue), kita juga akan membuat fungsi transformQueue
dengan type signature yang sama.
Array
, Tree
, Queue
, Blabla
semua ini bisa direpresentasikan dengan variable t
.
Sehingga jadilah sebuah fungsi generic baru untuk transformasi value di dalam suatu struktur data 🙂. Mari buatkan juga Type Class-nya agar t
dapat disubstitusi.
Class Transformable sudah dibuat. Fungsi transform
akhirnya bisa dipanggil oleh Array, Tree, dan Queue.
Terus Functor itu Apa?
Daritadi ngalor ngidul bahas “transformasi” tapi gak pernah nyinggung sama sekali tentang Functor. Eits jangan salah, sedari awal kita sebenarnya sudah bahas functor: Functor adalah class Transformable itu sendiri! Bedanya method transform
dinamai map
.
Laws
Jadi, Functor adalah sebuah type class dengan f :: Type -> Type
yang memiliki method tunggal map
. Plus, ada dua “hukum” yang harus dipatuhi agar benar dikatakan functor:
- Identity Law.
map identity f == f
- Composition Law.
map x (map y f) == map (x <<< y) f
Identity Law memastikan kesamaan struktur data setelah transformasi, walaupun value di dalamnya bisa jadi berbeda. Jangan terlalu ambil pusing sama hukum-hukum ini. Cukup tau aja 😄
Kind
Bagaimana dengan tipe data yang memiliki kind lebih dari Type -> Type
seperti Tuple atau Either? Apa masih bisa punya instance Functor?
Yap. Masih bisa dong. Apply secara partial saja, kan type constructor juga auto-curry di Purescript 😉 Sebagai konsekuensinya, hanya sisa type variable (yang paling kanan) saja yang bisa ditransformasi.
Penerapan
Penerapan Functor sangat sangat banyak. Sudah dimana-mana. Artikel ini bakal jadi puanjang kalau dibahas satu-satu 😅. Alih-alih saya buatkan list saja dan temen-temen bisa langsung intip ke TKP:
- Array
- Maybe
- Either
- Tuple
- Function!!
- Tree
- Queue
- NonEmptyList
- Parser
- endless possibilities..
Penutup
Saya kira sampai di sini dulu penjelasan tentang Functor. Saya harap kita belajar banyak dari Functor soal bagaimana HOF dan generic berperan penting dalam code reusability.
Ngomong-ngomong saya sedang menulis artikel lain tentang pembahasan apakah semua struktur data dengan kind Type -> Type
bisa otomatis menjadi Functor. Insight ini menarik karena dengan menganalisanya, kita bisa menemukan motivasi di balik Contravariant Functor, saudara tiri Functor. Insyallah rampung dalam waktu dekat.
Semoga series FP ini bermanfaat. Salam 🙂