Functional Programming: Mengenal Applicative Functor
dalam ES6
Kembali lagi kita bahas satu per satu konsep Functional Programming dalam JavaScript. Kali ini kita akan membahas Applicative Functor. Namun, untuk memahami konsep yang sebenernya sederhana ini (yang nggak semenyeramkan namanya), ada baiknya untuk terlebih dahulu paham apa itu Functor dan bagaimana cara kerjanya. Karena sejatinya, Applicative ini adalah subclass/turunan dari kategori Functor. Bagi yang ingin berkenalan dengan Functor atau sekedar mengingat-ingat kembali, bisa baca artikel saya disini:
Sekedar tambahan, di artikel ini juga ada sedikit pembahasan soal Monad. Maka saya rekomendasikan untuk setidaknya get the idea what Monad is:
Kasus Dasar
Langsung aja ke contoh kasus ya. Kita semua tahu bahwa Maybe
merupakan sebuah Functor. Dan salah satu cara untuk memodifikasi nilai dari suatu Functor adalah dengan memanggil fungsi map
-nya. Sebagai contoh:
const add = (x, y) => x + y
const incr = a => add(1, a)const maybeOne = Maybe(1)
const maybeTwo = maybeOne.map(incr) // => Maybe(2)
Bagaimana kalau kita coba lakukan operasi penambahan pada dua buah Functor?
const maybe1 = Maybe(1)
const maybe2 = Maybe(2)const maybe3 = maybe1.bind(satu =>
maybe2.map(dua => add(satu, dua))
)
// Maybe(3)
Bagaimana jika menambahkan 3 functor?
const maybe1 = Maybe(1)
const maybe2 = Maybe(2)
const maybe3 = Maybe(3)const maybe6 = maybe1
.bind(satu => maybe2
.bind(dua => maybe3
.map(tiga => add(satu, dua, tiga))
))
// Maybe(6)
So far so good. Namun kalau ditinjau lebih teliti, persamaan dari kedua solusi di atas adalah kita harus “membuka” masing-masing Functor secara manual (dengan memanggil .bind
dan .map
) agar nilainya dapat dijumlahkan dengan nilai Functor sebelumnya. Dan kemudian di Functor terakhir, memanggil function dan mengisi argument-nya dengan nilai-nilai yang sudah dibuka secara manual juga.
Adakah solusi yang lebih baik?
Penasaran dengan method
.bind
? Kamu bisa baca artikelnya disini
Sekilas Tentang Currying
Curried function adalah fungsi yang jika dipanggil namun belum lengkap argument-nya, maka akan mengembalikan fungsi baru dengan argument yang tersisa. Ada 2 cara untuk membuat curried function:
Manfaat currying akan terlihat sebentar lagi.
Now Introduce the Applicative Functor…
Kata kunci pertama agar mudah diingat:
Applicative Functor itu anggap saja sebagai function yang dibungkus oleh Functor
Jika method khas àla Functor adalah map
, dan Monad adalah bind
/flatMap
, maka operasi khas dari Applicative ini adalah ap
. Dengan sedikit modifikasi, kita bisa melakukan operasi yang jauh lebih readable seperti ini:
/* Kalau menambahkan 2 buah functor */
const add = curry((x, y) => x + y)
const maybe3 = pure(add)
.ap(maybe1)
.ap(maybe2)
/* Kalau menambahkan 3 buah functor */
const add3Nums = curry((x, y, z) => x + y + z)
const maybe6 = pure(add3Nums)
.ap(maybe1)
.ap(maybe2)
.ap(maybe3)
console.log(maybe3, maybe6)
// => Maybe(3), Maybe(6)
Ada 2 kata kunci baru, yang pertama adalah pure
, yang kedua adalah ap
. Pure hanya membuat argument-nya menjadi sebuah Functor (bisa Maybe, Either, IO, dll). Sehingga jika saya substitusi pure
dengan Maybe
, maka hasilnya akan sama saja:
const maybe3 = Maybe(add).ap(maybe1).ap(maybe2)
Tanpa currying, hal ini mustahil dilakukan. Karena fungsi add
akan dipanggil secara partial dengan dikirimi satu argument per satu pemanggilan .ap
. Begitu seterusnya sampai fungsi add
benar-benar fully-applied.
Minta implementasi fungsi ap
dong mas
Jangan kaget kalau implementasinya sangat sederhana, saking sederhananya saya kagum sama orang yang mencetuskan ide ini pertama kali, kok bisa bikin konsep simple tapi efeknya terasa sekali (lah curhat)
const ApFunctor = value => ({
map: f => ApFunctor(f(value)),
ap: otherFunctor => otherFunctor.map(value)
...
})
Sesederhana ini. Fungsi .ap
menerima Functor lain yang ingin dikirim isi-nya ke dalam function yang sudah di-pure (contoh di atas adalah fungsi add
). Step by stepnya:
const maybe3 = Maybe(add)
.ap(maybe1) // return maybe1.map(x => (y => x + y))
// jalankan map, maka x terisi oleh 1
// return Maybe(y => 1 + y) .ap(maybe2) // return maybe2.map(y => 1 + y)
// jalankan map, maka y terisi oleh 2
// return Maybe(1 + 2)
// return Maybe(3)
Bisa maen-maen disini kalau yang masih penasaran sama cara kerjanya. Klik aja link di bawah.
Jadi nggak perlu heran kenapa setiap 1 pemanggilan
.ap
hanya 1 argument saja yang disuplai, karena di dalam.ap
sendiri menjalankan operasi.map
yang notabene hanya membutuhkan 1 argument saja.
Cara Lain dengan Lift
Ini hanya alternatif saja. Kalau function yang mau kita lift (masukkan ke dalam Functor) memiliki 1 argument, maka aliasnya adalah liftA
. Kalau 2 argument maka liftA2
, kalau 3 maka liftA3
.
Yup betul ada liftA2
, dan liftA3
. Agak hard-coded ya. Saya kurang tahu siapa yang menamakan seperti ini pertama kali, tapi saya rasa penamaan ini cukup jelas dan mudah diingat. lift
, A
(Applicative), 2
(jumlah parameter).
Real World Example
Balik lagi ke contoh kasus offspring saya seperti di 2 artikel sebelumnya. Diberikan 2 buah data:
const munirFam = {
name: 'Munir',
spouse: 'Dian',
child: {
name: 'Jihad',
}
}const wawanFam = {
name: 'Wawan',
spouse: 'Imi',
child: {
name: 'Puspita',
}
}
Lalu kita ingin menikahkan anak dari keluarga Munir dengan anak dari keluarga Wawan, yaitu Jihad dengan Puspita (This is real, I’m getting married lol). Dan kemudian kita ingin membuat fungsi teks yang akan dibacakan oleh penghulu.
Yap, betul, hasil tidak seusai yang kita harapkan karena yang diterima dari getChild
adalah sebuah Functor, sedangkan getName
langsung mengakses attribute name
. Solusinya sangat mudah seperti yang sudah dijelaskan di atas, yaitu menggunakan Applicative Functor.
Selesai! Sangat simple. Dan jika salah satu keluarga ternyata belum meiliki anak, maka liftA2
akan mengembalikan Nothing
.
Manfaat
Ada beberapa keuntungan menggunakan konsep Applicative ini:
- Fungsi
textPenghulu
bisa fokus dengan business logic-nya sendiri. Dalam hal ini adalah string concatenation saja. - Simplicity. Code menjadi lebih concice dengan adanya Applicative.
- Untuk mengambil nilai dari satu atau lebih Functor, kita tidak lagi di-pollute oleh operasi buka-tutup Functor. Cukup lift function yang ingin di-apply dan sediakan Functor(s) kita untuk mengubah nilainya.
Intermezzo
Melihat implementasi ap
yang sangat sederhana, kita bisa menarik benang merah antara ap
dengan map
.
Namun jika bicara implementasi, karena JavaScript sendiri (ES6) belum support types, maka lebih mudah mengimplementasikan method liftA
untuk menggunakan Applicative. Alasannya adalah liftA
melakukan map fn
pada Functor pertama (argument kedua) yang di-pass. Jadi type-nya akan otomatis mengikuti Functor tersebut. Jika yang di-pass adalah Maybe, maka hasil dari liftA
adalah Maybe. Jika Either, maka hasilnya akan Either, dsb.
Contoh yang menggunakan
pure
di atas saya ambil dari Haskell, namun kurang cocok diterapkan di Javascript. Lebih tepat jika contoh di atas adalah(<$>)
-nya Haskell. Dan, fungsipure
idealnya sama denganliftA
yang menerima 2 parameter: Function dan Functor. Jadi saya rekomendasikan menggunakanliftA(n)
untuk kemudahan.
Kesimpulan
Sesuai dengan namanya, Applicative Functor adalah Functor yang bisa diaplikasikan. To recall, Function merupakan sesuatu yang bisa di-apply (diaplikasikan, dipanggil). Jika Function yang bisa di-apply ini dimasukkan ke dalam Functor, jadilah Applicative Functor.
Applicative Functor == Functor that holds a function
Penutup
Setelah panjang lebar belajar konsep Applicative (juga Functor & Monad), saya menjadi lebih percaya bahwa programming itu tidak hanya sekedar menulis code yang penting jalan. Kita juga musti jadi programmer yang baik, yang bisa membuat abstraksi dari permasalahan yang ada untuk membuat software yang lebih readable sekaligus maintainable.
Atau malah pingin kayak begini? 😛
Temen-temen punya pendapat lain? Let’s discuss in the comments section below!
If you like the article, please support us by hitting the 💚 button below. Thanks!