Code Reuse — Berkaca dari Functor

  •  7 min read
Image by <a href="https://pixabay.com/users/annca-1564471/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=3289812">annca</a> from <a href="https://pixabay.com/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=3289812">Pixabay</a>
Image by annca from Pixabay

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.

incr :: Int -> Int
incr n = n + 1
decr :: Int -> Int
decr n = n - 1
--
λ> incr 5
6
λ> decr 10
9

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?

incrArray :: Array Int -> Array Int
decrArray :: Array Int -> Array Int

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.

-- Implementasinya kita kosongkan dulu.
-- Kita asumsikan `arr` di-loop dan apply `editFn` di setiap elementnya
transformArray :: (Int -> Int) -> Array Int -> Array Int
transformArray editFn arr = ...

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.

incrArray :: Array Int -> Array Int
incrArray = transformArray incr
decrArray :: Array Int -> Array Int
decrArray = transformArray decr

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.

-- Before
transformArray :: (Int -> Int) -> Array Int -> Array Int
-- After
transformArray :: a b. (a -> b) -> Array a -> Array b

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 🎉🎉🎉

length :: String -> Int
asciiCode :: Char -> Int
isEven :: Int -> Boolean
--
λ> transformArray length ["jihad", "waspada"]
[5, 7]
λ> transformArray asciiCode ['A', 'B', 'a', 'b']
[65, 66, 97, 98]
λ> transformArray isEven [1, 2, 3, 4]
[false, true, false, true]
proud of myself
tepuk tangan dulu

Bentuk Lain

Kita sudah berhasil “menakhlukkan” Array. Saatnya membahas struktur data generic lain yang agak menantang. Let’s talk about Tree.

data Tree a = Leaf a | Branch a (Tree a) (Tree a)
treeExample :: Tree Int
treeExample = Branch 1
(Leaf 2)
(Branch 3 (Leaf 4) (Leaf 5))
<1>
/ \
<2> <3>
/ \
<4> <5>

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.

transformTree :: a b. (a -> b) -> Tree a -> Tree b
transformTree editFn tree = ...
--
λ> transformTree incr treeExample
Branch 2
(Leaf 3)
(Branch 4 (Leaf 5) (Leaf 6))

Fokus pada type signature, kita melihat pola yang sangat identik pada fungsi transformTree dan transformArray. Mereka identik dalam 2 hal:

  1. Behaviour (sama-sama sebagai fungsi transformasi)
  2. Type signature

Mungkin ketika suatu saat nanti ada struktur data lain (misal Queue), kita juga akan membuat fungsi transformQueue dengan type signature yang sama.

transformArray :: (a -> b) -> Array a -> Array b
transformTree :: (a -> b) -> Tree a -> Tree b
transformQueue :: (a -> b) -> Queue a -> Queue b
...
...
transformBlabla :: (a -> b) -> Blabla a -> Blabla b

Array, Tree, Queue, Blabla semua ini bisa direpresentasikan dengan variable t.

transform :: (a -> b) -> t a -> t b

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 t
transform :: a b. (a -> b) -> t a -> t b
instance transArray :: Transformable Array where
transform = transformArray
instance transTree :: Transformable Tree where
transform = transformTree
instance transQueue :: Transformable Queue where
transform = transformQueue
...
...
instance transBlabla :: Transformable Blabla where
transform = transformBlabla

Class Transformable sudah dibuat. Fungsi transform akhirnya bisa dipanggil oleh Array, Tree, dan Queue.

transform incr [1, 2, 3, 4]
transform incr (Branch 1 (Leaf 2) (Leaf 3))
transform incr (Queue [1, 2, 3, 4])

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.

class Functor f
map :: a b. (a -> b) -> f a -> f b

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:

  1. Identity Law. map identity f == f
  2. 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?

data Tuple a b = Tuple a b
data Either a b = Left a | Right b
λ> :k Tuple
Type -> Type -> Type
λ> :k Either
Type -> Type -> Type

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.

instance functorTuple :: Functor (Tuple a) where
map fn (Tuple a b) = Tuple a (fn b) -- `a` is unchanged
instance functorEither :: Functor (Either a) where
map _ (Left a) = Left a -- unchanged
map fn (Right b) = Right (fn b)

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:

  1. Array
  2. Maybe
  3. Either
  4. Tuple
  5. Function!!
  6. Tree
  7. Queue
  8. NonEmptyList
  9. Parser
  10. 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 🙂