Gambar buatan sendiri nih :))

Apa itu Functor?

Eksplorasi konsep Functor dengan ES6

Jihad Dzikri Waspada
codewey
Published in
8 min readFeb 18, 2017

--

Lumrahnya, istilah Functor dalam dunia programming baru akan kita dengar ketika mulai mendalami Functional Programming. Tapi gak perlu takut dulu, karena Functor sejatinya hanyalah sebuah konsep dasar yang mungkin sering kita jumpai sehari-hari. Functor akan sangat membantu kita dalam penulisan kode yang lebih mudah dibaca, lebih ringkas, dan lebih maintainable. Functor secara tidak langsung juga akan mengurangi penggunaan if-else dalam kode, sehingga kemampuan problem solving kita dapat lebih bervariasi.

Jika kamu cukup familiar dengan pengolahan array pada Javascript seperti map, reduce, filter dan sejenisnya, selamat, karena pada dasarnya array pada Javascript adalah sebuah Functor!

Note: bagi yang belum familiar dengan operasi map di Javascript, saya sarankan untuk memahaminya terlebih dahulu agar lebih mudah mencerna konsep Functor

Jadi, Functor itu apa?

According to Haskell and the Fantasy Land specification, a functor is simply something that can be mapped over. In OOP-speak, we’d call it a ‘Mappable’ instead. — This post

Sebelum masuk lebih dalam tentang ‘sesuatu yang bisa di-map-kan’ atau Mappable, saya ingin menggambarkan Functor itu seperti sebuah wrapper. Kita bisa memasukkan nilai apapun ke dalam wrapper tersebut, bisa integer, string, object, array .. sembarang. Ibaratnya, kita punya barang (value) yang dibungkus oleh kado (wrapper). Nah, kado inilah yang kita sebut dengan Functor.

Functor pada dasarnya mempunyai 2 kemampuan:

  1. unit/of: untuk membungkus barang dengan kado
  2. map: untuk membuka kadonya

Contoh yang paling mudah untuk memahami konsep bungkus + bongkar nya Functor adalah fungsi map pada Javascript:

const lima = [5]
const tujuh = lima.map(x => x + 2)
console.log(tujuh)
// => [7], bukan 7

Baris pertama adalah proses pembungkusan angka 5 dengan array (nomor 1: unit/of), kemudian baris selanjutnya adalah proses membuka bungkus array yang melekat pada 5 dengan fungsi map (nomor 2: map). Ketika sudah terbuka, tambahkan dengan 2, dan bungkus hasilnya lagi dengan kado array lain, sehingga hasil akhirnya adalah [7] . Step by step:

  1. Bungkus 5 -> [5]
  2. Buka bungkusannya dengan map [5] -> 5
  3. Proses nilainya 5 + 2
  4. Bungkus lagi hasilnya menjadi [7] , sehingga [5] -> [7]

Contoh lain, saya akan map sebuah Functor bernama JamDinding:

Map pada sebuah Functor JamDinding. Nilai boleh beda, tapi struktur tetap sama

Ilustrasi di atas memperlihatkan bahwa nilai pada gambar sebelah kanan berbeda dari gambar sebelah kiri, namun struktur wrapper-nya tidak berubah: Bulat, berwarna biru, dan menyimpan 12 nilai.

Sehingga, jika saya menjalankan map sebuah Functor, nilai kembaliannya boleh berbeda dengan nilai awal, pun tipe datanya boleh berbeda, namun yang menjadi catatan penting adalah nilai kembalian tersebut harus dibungkus dengan wrapper baru yang struktur/bentuknya sama.

Masuk ke teori, bila saya substitusi sebuah Functor dengan huruf F dan Functor yang diisi dengan sebuah nilai (bisa Integer, Double, Boolean, String, dsb) itu dengan F[A], maka fungsi map pada Functor akan bernotasi F[A].map((A) => B): F[B]. Sehingga untuk kasus Array, bisa saya tuliskan sebagai Array[X].map((X) => Y): Array[Y].

Bagi yang masih belum familiar dalam membaca notasinya, beberapa contoh dibawah ini mungkin dapat membantu:

const lima = [5]
const enam = [6.00]
// Array[Int].map((Int) => Int): Array[Int]
const tujuh = lima.map(x => x + 2)
console.log(tujuh) // [7]
// Array[Int].map((Int) => String): Array[String]
const maroon5 = lima.map(x => `Maroon${x}`)
console.log(maroon5) // ['Maroon5']
// Array[Double].map((Double) => Array): Array[Array]
const thirty = enam.map(y => [y * 5])
console.log(thirty) // [[30]]

Saya kira kode di atas sudah cukup jelas untuk mendeskripsikan sifat map pada Functor 😁

Sebelum dan setelah di-map, nilai dan tipe data boleh berbeda (misal dari Int ke String), tapi struktur wrapper-nya haruslah tetap sama (misal harus sama-sama Array).

Gimme the Syntax!

Kira-kira, Functor berparas seperti ini:

class MyFunctor {
constructor(value) {
this.value = value
}
static of(value) { // bisa 'of' bisa 'unit'
return new MyFunctor(value)
}
map = (fn) => MyFunctor.of(fn(this.value)) get = () => this.value
}

Melihat implementasi method map di atas, tidaklah heran kalau ia mempunyai nilai kembalian yang setipe (dalam hal ini tipenya adalah MyFunctor). Kalau method map di atas diganti dengan:

map = (fn) => AnotherFunctor.of(fn(this.value))

maka ini adalah map yang salah, karena yang memiliki method map ini adalah MyFunctor, sedangkan tipe kembaliannya adalah AnotherFunctor. Illegal.

Why Functor?

Setelah ngalor-ngidul membahas dari segi teorinya, biasanya kita-kita ini baru ngeh kalo sudah tau seperti apa kegunaannya di dunia nyata. Nah sekarang mari kita bahas kenapa sih ada konsep Functor dan kenapa harus kita manfaatkan.

Polymorphism

Membuat sebuah fungsi bukanlah hal yang sulit:

const addOne = (number) => number + 1console.log(addOne(2))
// => 3

Tapi bagaimana kalau saya ubah konteksnya dengan sebuah array? object? atau bahkan null? Akankah fungsi addOne tetap mengeluarkan output sesuai yang saya harapkan?

console.log(addOne([2]))        // => '21' 
console.log(addOne({ num: 2 })) // => '[object Object]1'
console.log(addOne('2')) // => '21'
console.log(addOne(null)) // => 1
console.log(addOne([2, 3, 4])) // => '2,3,41'

Agar fungsi addOne dapat berjalan sesuai yang diharapkan, cara yang paling mudah adalah dengan memodifikasi function body-nya dengan classic if-else terhadap tipe inputannya.

const addOne = (number) => {
if (typeof number === 'string') return parseInt(number) + 1
if (typeof number === 'array') return number[0] + 1
if (typeof number === 'object') return ..
if (number === null) return ..
// dsb..
}

Hmm, sepertinya bukan solusi yang cukup bagus. Sekarang mari kita coba dengan Functor.

const intFunctor = MyFunctor.of(2)
const res = intFunctor.map(addOne).get()
console.log(res)
// => 3
const arrayFunctor = [2]
const res2 = arrayFunctor.map(addOne)
console.log(res2)
// => [3]
const manyArrayFunctor = [2, 3, 4]
const res3 = arrayFunctor.map(addOne)
console.log(res3)
// => [3, 4, 5]

Woah! Ternyata sejauh ini outputnya sesuai harapan. Lalu, bagaimana jika saya pass sebuah object? Mari buat Functor sederhana untuk object (sumber):

class ValueMappable {
constructor (object) {
this.object = object
}
of = (obj) => new ValueMappable(obj) map (fn) {
const mapped = { }
for (const key of Object.keys(this.object)) {
mapped[key] = fn(this.object[key])
}
return ValueMappable.of(mapped)
}
get = () => this.object
}

Kemudian kembali lagi ke fungsi addOne:

const objFunctor = ValueMappable.of({ one: 1, two: 2 })
const res4 = objFunctor.map(addOne).get()
console.log(res4)
// => { one: 2, two: 3 }

Dengan Functor kita tidak perlu memodifikasi addOne agar bisa menangani berbagai macam konteks, kita hanya perlu mengubah konteksnya di luar function dan fokus membuat business logic di dalamnya.

Kasus lain: Saya memiliki sebuah object family { father, mother, me } yang di-encode. Saya ingin men-decode object tersebut, transformasi ke huruf kapital, lalu sambut mereka dengan menambahkan tulisan ‘Welcome’ di depannya.

import _ from 'lodash'const fromNumberToString = (number) => {
const alpha = ['a', 'b', 'c' ... 'z']
return number.map(n => alpha[n - 1]).join('')
}
const capitalize = (text) => _.capitalize(text)
const welcome = (name) => `Welcome, ${name}!`
// Map!
const welcomeAnEncodedFam = (persons) =>
new ValueMappable(persons)
.map(fromNumberToString)
.map(capitalize)
.map(welcome)
.get()
const encodedFamily = {
father: [13, 21, 14, 9, 18],
mother: [4, 9, 1, 14],
me: [10, 9, 8, 1, 4]
}
console.log(welcomeAnEncodedFam(encodedFamily))
// => {
// father: 'Welcome, Munir!',
// mother: 'Welcome, Dian!',
// me: 'Welcome, Jihad!'
// }

Kita bahkan bisa reuse fromNumberToString, capitalize, dan welcome untuk digunakan oleh Functor lain:

const encodedFamily = [
[13, 21, 14, 9, 18],
[4, 9, 1, 14],
[10, 9, 8, 1, 4]
]
console.log(
encodedFamily
.map(fromNumberToString)
.map(capitalize)
.map(welcome)
)
// => ['Welcome, Munir!', 'Welcome, Dian!', 'Welcome, Jihad!']

Nah, dari contoh-contoh di atas, saya menyimpulkan bahwa kita bisa menerapkan polymorphism dalam Javascript terhadap segala tipe/struktur data dengan menggunakan Functor. Jika saya melihat sebuah variabel dan tahu itu Functor, maka saya hanya perlu memanggil .map untuk memodifikasi value-nya, tanpa perlu terlalu dalam mengetahui internal proccess Functor tersebut. It just works!

Safety

Manfaat lainnya dari penerapan Functor adalah safety, terutama ketika berhadapan dengan null dan undefined.

Contoh kasus: Saya memiliki silsilah keluarga yang strukturnya seperti ini:

const jihadOffspring = {
name: 'jihad',
spouse: 'puspita',
child: {
name: 'fatimah',
spouse: 'ahmad',
child: {
name: 'furqon'
}
}
}
const aniOffspring = {
name: 'ani',
spouse: 'abdan',
child: {
name: 'ana',
spouse: 'antum',
}
}

dan saya ingin mengucapkan welcome kepada cucu seseorang. Kalau jihad berarti cucunya bernama furqon, dan kalau ani karena belum mempunyai cucu, maka kembalikan string kosong. Kita akan tetap menggunakan fungsi-fungsi dari contoh-contoh sebelumnya. Tanpa Functor, akan tertlihat seperti ini:

const getName  = (person) => person.name
const getChild = (person) => person.child
const getGrandChildName = (fam) => {
let name = '';
const child = getChild(fam);
if (child) {
const grandChild = getChild(child);
if (grandChild) {
name = getName(grandChild)
}
}
return name
}
const cucuJihad = getGrandChildName(jihadOffspring)
const cucuAni = getGrandChildName(aniOffspring)
console.log(welcome(capitalize(cucuJihad)))
// => 'Welcome, Furqon!'
console.log(welcome(capitalize(cucuAni)))
// => 'Welcome, !'
// Oops.. 🙈

Untuk menghindari error Uncaught TypeError: Cannot read property ‘child’ of undefined, fungsi getGrandChildName perlu melakukan pengecekan beberapa kali, sebelum akhirnya mengembalikan nama seorang cucu. Bahkan, operasi welcome pun seharusnya melakukan if-else terlebih dahulu supaya menghasilkan output yang benar. Bagaimana caranya mengakali kasus seperti ini?

Intoduce Maybe Functor!

Saya akan membuat Functor Maybe sederhana yang kira-kira seperti ini:

class Maybe {
constructor(value) {
this.value = value
}
static of(value) {
return new Maybe(value)
}
map(fn) {
if (this.isNothing()) {
return Maybe.of(null)
}
return Maybe.of(fn(this.value))
}
isNothing = () => this.value === null || this.value === undefined get = () => this.value getOrElse(elseValue) {
if (this.isNothing()) {
return elseValue
}
return this.get()
}
}

Kembali ke contoh kasus ‘offspring’ tadi, mari kita perhatikan bedanya:

const getName  = (person) => person.name
const getChild = (person) => person.child
const cucuJihad =
Maybe.of(jihadOffspring)
.map(getChild)
.map(getChild)
.map(getName)
.map(capitalize)
.map(welcome)
.getOrElse('')
console.log(cucuJihad)
// => 'Welcome, Furqon!'
const cucuAni =
Maybe.of(aniOffspring)
.map(getChild)
.map(getChild) // returns Maybe.of(undefined)
.map(getName) // returns Maybe.of(null)
.map(welcome) // returns Maybe.of(null)
.getOrElse('')
console.log(cucuAni)
// => ''

Pun jika kita flashback dengan fungsi .map(fn) pada array, kita akan menemukan behaviour yang serupa:

const kopong = []
const res = kopong
.map(x => x + 2)
.map(y => y * 4)
.map(z => String(z))
console.log(res)
// => []

No ifs, no nested ifs, no elses, no frowned faces, simpler, clean code, much more readable, easy to maintain, and everyone’s happy 😃

Hukum Functor

Selain .map(fn) harus mengembalikan nilai di dalam struktur yang sama, ada beberapa ketentuan lain untuk object yang mengimplementasikan .map(fn) agar benar bisa dikatakan sebuah Functor.

The identity law:

functor.map(x => x) ≡ functor

maksud dari notasi ini adalah jika sebuah functor di-map dan langsung mengembalikan nilainya (tanpa modifikasi), maka nilai kembaliannya pastilah sama dengan nilai awalnya.

[1, 2, 3].map(x => x) === [1, 2, 3]

The composition law:

functor.map(f).map(g) ≡ functor.map(x => g(f(x)))

Kita bisa meringkas beberapa .map(fn) sekaligus dengan hanya memanggil satu .map(fn) saja, yaitu dengan memanggil fungsi-fungsi yang ingin dijalankan di dalam fn map tersebut seperti yang biasa kita lakukan.

const addOne    = (number) => number + 1;
const timesFour = (number) => number * 4;
[3].map(addOne).map(timesFour) // [16]
[3].map(x => timesFour(addOne(x))) // [16]

Bahkan, berkaca pada contoh kasus ‘offspring’ sebelumnya, dengan hukum ini, kita dapat membuat composable function baru untuk mendapatkan object cucu, sebut saja getGrandChild:

const getChild = (person) => person.child;
const getGrandChild = (person) => getChild(getChild(person))
const cucuJihad =
Maybe.of(jihadOffspring)
.map(getGrandChild) // bukan lagi .map(getChild).map(getChild)
.map(getName)
.map(capitalize)
.map(welcome)
.getOrElse('')
console.log(cucuJihad)
// => 'Welcome, Furqon!'
const cucuAni =
Maybe.of(jihadOffspring)
.map(getGrandChild)
.map(getName)
.map(capitalize)
.map(welcome)
.getOrElse('')
console.log(cucuAni);
// => ''

Kesimpulan

Beberapa key points yang bisa kita ambil adalah:

  1. Functor adalah sesuatu yang mengimplementasikan .map(fn)
  2. Array mengimplementasikan .map(fn), berarti ia Functor
  3. Nilai kembalian dari functor.map(fn) boleh berbeda, namun struktur wrapper-nya harus tetap sama

Dan menurut saya pribadi (mudah-mudah temen-temen juga demikian), Functor tidaklah sesulit yang dibayangkan sebelumnya, dan merupakan salah satu solusi cerdas dalam menangani null dan undefined dengan cara yang clean dan elegan. Sebetulnya masih ada beberapa bentuk Functor lagi selain Maybe(Just, Nothing), ada Either(Left, Right), Validation(Success, Fail), dan mungkin Promise(Resolve, Reject).

Semoga tulisan yang panjang ini dapat bermanfaat buat temen-temen sekalian dan meningkatkan kualitas code kita kedepannya.

**PS: don’t forget to click the heart button below :)**

--

--

Jihad Dzikri Waspada
codewey

Software Developer @Chordify, Utrecht. NOTE: Please navigate to https://jihadwaspada.com. I no longer write on Medium