Monad in Haskell (source)

Functional Programming: Mengenal Konsep Monad (Maybe + Either)

Seperti biasa, pake ES6

Don’t fear the Monad! Mungkin inilah kalimat pertama yang harus saya ucapkan ketika bicara Monad. FP (Functional Programming) emang suka ngebuat kita pusing dengan istilah-istilah aneh: ada Monad, Functor, Applicative, Semigroup, Monoid, dll. But fear not! Karena kali ini saya akan coba explore Monad dalam JavaScript agar lebih mudah dipahami.

What?

Sebelumnya, perlu saya sampaikan bahwa Monad itu merupakan turunan dari kategori Functor. Functor => Applicative => Monad. Atau kalo boleh bicara dalam scope OOP, Monad itu seperti Monad implements Applicative dan Applicative implements Functor. Untuk perkenalan apa itu Functor, mari berkelana kesini dulu:

Untuk memahami Monad, saya sarankan untuk mengerti dulu soal Functor. Karena Functor merupakan dasar dari konsep value wrapping yang nantinya juga akan digunakan oleh Applicative dan Monad.

Untuk Applicative, yang penasaran bisa mampir kemari.

Nah, kilas balik sebentar, bagi yang sudah paham, Functor adalah suatu struktur data yang memiliki fungsi map dengan type signature

F[A].map(fn: A => B): F[B]

Karena Monad adalah bagian dari Functor, otomatis Monad juga memiliki method yang ada pada Functor, namun dengan beberapa tambahan. Jika disederhanakan, Monad memiliki struktur seperti ini:

class Monad {
of(val) { .. }
map(fn) { .. } // fungsi turunan Functor
flatMap(fn) { .. } // fungsi punya Monad, biasa disebut bind
}

Yang perlu diperhatikan adalah fungsi fn yang dikirim sebagai paramater map dan flatMap/bind. Pada map, fn memiliki tipe (A => B) sedangkan pada flatMap, fn harus bertipe (A => Monad[B]) yang berarti nilai kembalian dari fungsi fn wajib ber-instance Monad. Karena Maybe yang merupkan Functor juga merupakan instance dari Monad, maka mari gunakan doi sebagai contoh:

Maybe.of(2).map(x => `${x + 1}`)
// Maybe.of('3')
// map menerima fungsi bertipe (Int => String)
Maybe.of(2).flatMap(x => Maybe.of(`${x + 1}`))
// Maybe.of('3')
// flatMap menerima fungsi bertipe (Int => Maybe[String])
To conclude,
F[A].map(fn: A => B): F[B]
M[A].flatMap(fn: A => M[B]): M[B]

Simple, kan? 😁


Struktur

Implementasi flatMap itu sebenernya cukup sederhana, dia hanya melakukan proses flatten terhadap map, sesuai namanya.

const Monad = value => ({
map: fn => Monad(fn(value)),
flatMap(fn) { return this.map(fn).join() },
join: () => value
})
Monad.of = val => Monad(val)

Tanpa operasi join (proses flattening), Maybe.of(2).map(x => Maybe.of(`${x + 1}`)) akan menghasilkan Maybe(Maybe('3')) oleh karena itu, untuk menghasilkan Maybe('3') kita musti hilangin 1 level dengan melakukan join.

Andaikata JavaScript mempunyai type system, method join seharusnya ber-type signature M[M[A]].join(): M[A]. Tapi karena kenyataannya tidak demikian, walhasil join jadi bisa dipanggil sebagai M[A].join(): A, yang sebetulnya illegal dilakukan

Real World Example

Langsung ke contoh aja ya. Kita flashback sebentar dengan membahas Maybe Monad. Dan mulai sekarang saya akan gunakan istilah bind sebagai ganti dari flatMap. Tujuannya ya agar kita terbiasa dengan istilah ini karena bacaan-bacaan tentang Monad banyak yang memilih menggunakan bind daripada flatMap. It’s just a term.

Maybe Monad

Saya berikan contoh kasus sederhana seperti yang ada pada artikel saya sebelumnya, dimana ada data berupa

const aniOffspring = {
name: 'ani',
spouse: 'abdan',
child: {
name: 'ana',
spouse: 'antum',
}
}

dan kita ingin mendapatkan nama cucu Ani. Namun sayangnya, Ani belum mempunyai cucu.

const getChild = person => person.child
const cucu = getChild(getChild(aniOffSpring))
cucu.toUpperCase()

cucu akan bernilai undefined. Dan cucu.toUpperCase() akan mengeluarkan error Cannot read property ‘toUpperCase’ of undefined.

Solusi menggunakan Maybe:

Maybe memiliki 2 ‘anak’, yaitu Just dan Nothing (atau dalam istilah lain Some dan None). Jika nilai yang ingin kita wrap bersifat falsy (bisa null, undefined, atau berupa string kosong), maka kita gunakan Nothing. Sebaliknya, jika ada nilainya, maka gunakan Just. class Maybe disini kita gunakan sebagai factory function saja:

Maybe Monad

Dari code snippet di samping, untuk menanggulangi masalah cucu Ani yang belum lahir, maka kita bisa buat solusinya dengan mudah dan elegan:

const cucu = 
Maybe.of(aniOffspring)
.map(getChild)
.map(getChild)
.orJust('blom lahir')
cucu.toUpperCase()

dan hasilnya adalah BLOM LAHIR. Simple kan. Itu jika menggunakan Functor…

[IMPORTANT] Tapi menurut saya, fungsi getChild tidak terlalu mendeskripsikan apa yang dilakukannya. Ia langsung mengembalikan person.child seolah-olah semua person sudah pasti memiliki child. Sedangkan kenyataannya adalah tidak semua person memiliki child. Bisa jadi punya, bisa jadi tidak punya. Atau bahasa inggrisnya, Maybe he has a child, maybe he doesn’t.

Let’s redefine the getChild function agar fungsi ini benar-benar menggambarkan apa yang dilakukannya:

const getChild = person => Maybe.of(person.child)

Nah kalau sudah begini kan enak: temen kerja si programmer jadi tau dengan jelas bahwa object person ini bisa punya attribute child bisa juga enggak. Dan karena getChild yang baru ini punya tipe kembalian Monad, kita gak bisa lagi dong menggunakan map, kita musti make bind. Walhasil, solusi sebelumnya harus kita update menjadi:

const cucu = 
Maybe.of(aniOffspring) // Just { name: 'ani' .. }
.bind(getChild) // Just { name: 'ana' .. }
.bind(getChild) // Nothing
.orJust('blom lahir')
cucu.toUpperCase() // BLOM LAHIR

Baru deh keliatan kegunaan bind setelah panjang-panjang baca 😨.

Either Monad

Sama seperti Maybe, Either Monad juga berfungsi sebagai wrapper ketika suatu nilai tidak memenuhi persyaratan. Kalau Maybe syaratnya adalah nilai yang ingin di-wrap tidak boleh bersifat falsy, maka Either ini syaratnya adalah bebas alias kita sendiri yang menentukan, plus kita juga bisa memberikan pesan error yang bervariasi sekaligus. Sifat dari Either ini adalah fail fast, yang berarti pesan error tidak diakumulasi sampai akhir.

Contoh kasus: User login. Ketika login, biasanya user mengirimkan 2 parameter yaitu username dan password:

const auth = {
username: 'someusername',
password: 'somepassword',
}
constraints:
1. username tidak boleh kosong
2. password tidak boleh kosong
3. username harus 'jihad'
4. password harus 'jihad123'
after:
1. tampilkan pesan error jika melanggar salah satu constraint
2. tampilkan "Welcome back, ${username}!" jika berhasil

Jika kita ingin memvalidasi object auth di-atas secara imperative, maka kira2 hasilnya bakal begini:

const validateLogin = (auth) => {
const { username, password } = auth
  if (!username) return 'error: No Username'
if (!password) return 'error: No Password'
if (username !== 'jihad' && password !== 'jihad123')
return 'error: Username dan password mismatched'
  return `Welcome back, ${username}!`
}

So far so good. Tapi bagaimana jika ternyata dua bulan kemudian Tim Produk membuat keputusan bahwa username harus lebih dari 6 karakter? Ubah lagi if-nya. Bagaimana jika ternyata kemudian Tim Produk bilang password harus mengandung huruf, angka, dan karakter? Ubah lagi if-nya. Bagaimana jika format errornya berubah misal dari error: msg jadi object { error: msg }? Ubah lagi semua return-nya. Dan seterusnya. Kalau menurut saya, dengan mengubah code terlalu banyak di dalam fungsi validateLogin, bisa-bisa kita melanggar OCP (Open-Closed Principle), prinsip kedua dari SOLID.

Namun, jika kita melakukannya dengan pendekatan FP, kita bisa meng-extend fungsi tersebut dengan Monad. So please welcome The Either Monad:

Either Monad

Dengan implementasi sederhana di atas, kita bisa membuat code yang chainable dan/atau extendable.

Oh ya, dengan catatan, method cata tersebut merupakan singkatan dari catamorphism. Jika Right, maka jalankan argument kedua, jika Left, maka jalankan argument pertama.
Solusi menggunakan Either Monad

Solusi yang saya tawarkan dengan menggunakan Either Monad adalah seperti code snippet disamping.

So, mari bawa kembali pertanyaan-pertanyaan tadi. Bagaimana jika menambah validasi username harus lebih dari 6 karakter? Tinggal buat fungsi baru bernama checkUsernameLength. Bagaimana jika format errornya berubah misal dari error: msg jadi object { error: msg }? Tinggal ubah fungsi formatError saja, ndak perlu semua tempat kita ubah 😃

Jika kita harus mengubah validateLogin, kita hanya perlu menambahkan fungsi baru saja, tidak perlu ubah secara hard-code seperti solusi sebelumnya.

Atau bahkan, kita dapat membuat fungsi checkUsername dan checkPassword menjadi reusable (kali aja kepake juga di fungsi lain macam validateRegister misalkan) sehingga code yang kita buat justru berasa lebih ganteng:

More reusable functions with constraints

Wrap Up

Yak jadi kira-kira begitulah kegunaan Monad sebagai struktur data — dapat menyediakan abstraksi yang memisahkan antara data dan function yang digunakan untuk memanipulasi data tersebut. Sehingga function kita bisa jadi lebih reusable. Plus, tujuan utama dari Monad adalah untuk menjalankan suatu operasi dengan cara yang lebih imperatif (chaining), biar lebih readable aja gitu. Bagi yang pernah bergulat dengan Promise, pasti tau tuh fungsi then(res) dan catch(e) dan cara aksesnya yang mirip dengan Monad:

Promise(resolve, reject)
.then(..)
.then(..)
.catch(..)
vs
Monad.of(value)
.bind()
.bind()
.orSome() // atau
.cata(left, right)

dan ternyata banyak juga yang bilang Promise itu salah satu implementasi dari Monad 😃


Penutup

Sebenernya masih banyak macam-macam Monad selain Maybe dan Either. Masih ada IO (untuk side-effect), Reader (untuk Dependency Injection), Writer (dokumentasi operation), Transformer (Ubah tipe Monad ke Monad lain), State, Free, dll yang saya sendiri juga belum tau banyak.

Laws dari Monad pun tidak saya bahas disini karena dikhawatirkan membuat pembaca dan juga penulis malah menjadi bingung haha.

Pun tulisan beserta contoh-contohnya sengaja saya buat sesederhana mungkin dan kalau ada yang dirasa oversimplified monggo bisa kita diskusikan di kolum komentar. Atau kalau ada point-point yang bagus untuk saya masukkan di tulisan ini, boleh juga, seneng malah hehe.


Semoga tulisan ini dapat bermanfaat dan membuka wawasan baru dengan pendekatan Functional Programming. Yang paling penting, kita jadi bisa melatih kemampuan abstraksi kita dalam memecahkan berbagai macam permasalahan dunia programming.

Selamat berpuasa. Selamat berbuka. Mohon maaf lahir batin. Dan, Happy coding!

If you like it, please support us by hitting the green button below 💚

Tangerang, Ramadhan 28th, 1438 written in 6 non-stop hours.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.