Functional Programming: Dependency Injection menggunakan Reader Monad π
with the help of Typescript
Salam semua! Kembali lagi di episode Functional Programming dengan Javascript! Yeeaaay πππ. Namun kali ini kita akan coba perlahan menggunakan Typescript, karena gak akan pernah afdhol kalo bicara FP tapi gak menyinggung types-nya π
Apa dan Mengapa DI?
Bagi yang belum tahu apa itu DI, bisa dibaca dulu apa sih Dependency Injection dan mengapa DI itu penting dalam Software Development. Scope artikel ini hanya mengulas bagaimana melakukan DI the functional way, itu juga kalo bener π (mohon koreksinya suhu Wisnu Adi Nurcahyo ππ»)
Apa: design patterns β What is dependency injection? β Stack Overflow
Mengapa:
Intinya: DI memungkinkan component/class/function yang kita buat tidak bergantung pada 1 instance tertentu saja, sehingga bisa di-substitute dengan instance yang lain β‘οΈ more flexibility.
Nah, bicara tentang Dependency Injection, sebenarnya banyak kok cara melakukan DI di Javascript. Cara paling mudah adalah dengan inject dependencies-nya ke class constructor seperti yang dicontohkan di artikel Mas Wahyudi Wibowo barusan:
class UserRepo {
constructor(db) { // <- HERE
this.db = db
} ... save(user) {
this.db.insert(user)
} ...}
Atau cara kedua seperti yang Mattias terangkan di video ini? (alert: 22 mins video)
Yang kurang lebih, bisa saya simpulkan seperti:
const save = (user, db) => db.insert(user) // atau
const save = user => db => db.insert(user)saveUserToDb = save(user);
...
...
saveUserToDb(db)
Sebenarnya ndak ada masalah, mungkin dengan cara di atas sudah cukup. Yang penting jalan kan hehe. Tapi muncul pertanyaan: apakah solusi tersebut sudah mendukung composability? Karena tujuan menggunakan pendekatan Functional Programming salah satunya adalah membuat program yang composable, sehingga kode bisa menjadi lebih rapi dan reusable. Dan, cara paling mudah untuk membuktikannya adalah dengan membahas sebuah case study.
Case Study
Sebut saja Bambang (bukan nama sebenarnya), dia menjual gorengan menggunakan boraks. Ermm, nggak. Sesungguhnya dia adalah seorang nasabah dari sebuah bank bernama Bank Krut. Karena dia jomblo dan kere karena pake boraks, dia memutuskan untuk menabung hasil jerih payahnya di bank tersebut untuk modal pesta pernikahannya. Transaksi Bambang terhitung irit, kamu bisa lihat betapa bahagianya dia:
Oh iya, buat kamu-kamu yang masih kadang bingung bedanya debit sama credit (kayak gue π) debit itu nabung, credit itu narik duid.
Okay, balik ke pembahasan. Ada beberapa issue yang terlihat dari kode barusan:
- Terlalu verbose. Orang yang nanti akan menggunakan fungsi-fungsi tersebut harus menyuplai dependency satu per satu. Terasanya belakangan ketika sudah banyak fungsi yang terlibat.
- Sulit untuk di-compose. Seperti yang kita ketahui, salah satu keuntungan menggunakan pendekatan FP adalah composability-nya.
Adakah cara lain agar kodenya tidak terlalu verbose namun tetap bisa melakukan komposisi?
Reader Monad!
Reader Monad bisa menjadi salah satu alternatif untuk permasalahan ini. Walaupun gak harus jadi silver bullet, ada baiknya kita memahami konsep Reader Monad ini dan melihat apakah bisa menawarkan solusi yang lebih baik. Buat temen-temen yang sudah lupa atau belum tahu apa itu Monad, bisa di-recall dulu apa sih definisi dan kegunaan Monad
Nah setelah fresh dengan method fmap
dan bind
, kita bisa lanjut membahas Reader Monad yang ternyata simple tapi cukup bikin dahi mengernyit. Dan sebelum masuk ke solusi sekaligus contoh pemakaiannya, kita akan coba mempelajari bagaimana membuat Reader Monad from scratch.
Step 1: See the Pattern
Mari kita mulai: langkah pertama dalam mecari suatu solusi adalah dengan melihat pola atau pattern dari masalah yang ingin dipecahkan. Dalam hal ini, pola yang terlihat adalah banyaknya pemanggilan terhadap dependency-nya ...(repo)
. Bila kita scroll sedikit ke atas dan merenunginya (awas ada mantan lewat), inti dari fungsi-fungsi tersebut adalah menerima dependency di argumen-nya, kemudian mengembalikan suatu nilai, sebut saja A
.
const fungsi = <A>(deps: Dependencies): A => { ... }
Mari kita simpan notasi ini dan sepakat menyebutnya dengan nama Reader, sesuatu yang akan membaca/menggunakan dependencies (environment).
So, untuk kasus transaksiBambang
ini, bisa kita pahami
const transaksiBambang = (no: string) => (deps: D) => { return A }const transaksi = transaksiBambang(no)
// transaksi: (deps: D) => { return A }
// transaksi adalah Reader<D, A>
Step 2: Add fmap Function!
Karena Monad merupakan salah satu turunan dari Functor, wajib hukumnya untuk menuliskan implementasi fmap. Masih ingatkan notasi dari fmap
?
So, bisa kita mulai dari return type-nya dulu. Karena return type-nya adalah Reader<D, B>
yang merupakan alias dari (deps: D) => B
, maka:
fmap = (f, readerA) => Reader<D, B>
fmap = (f, readerA) => ((deps: D) => B)
Sekarang nilai B
. f
memiliki type A => B
yang berarti dengan melakukan f(A)
kita akan mendapatkan nilai B
.
fmap = (f, readerA) => ((deps: D) => B)
fmap = (f, readerA) => ((deps: D) => f(A))
Dan tentu saja nilai A
didapat dari pemanggilan readerA(deps)
melihat type-nya yang readerA: (deps: D) => A
.
fmap = (f, readerA) => ((deps: D) => f(A))
fmap = (f, readerA) => ((deps: D) => f(readerA(deps)))
Sampai sini dapat disimpulkan fungsi fmap
untuk Reader Monad adalah:
ITβS EASY! ππ
Part 3: BIND! The Fun Part
Sekarang saatnya kita membuat Reader ini menjadi Monad dengan mengimplementasikan fungsi bind
. Method bind
pada Monad memiliki type signature:
Hampir tidak ada perbedaan: return type-nya sama-sama Reader<D, B>
. Yang membedakan hanya fungsi f
-nya saja (dibahas sebentar lagi). Namun memiliki return type yang sama: Reader<D, B>
a.k.a (deps: D) => B
.
bind = (f, readerA) => ((deps: D) => B)
Sebelumnya pada fungsi fmap
, f(reader(deps))
akan menghasilkan B
.
fmap = (f, readerA) => ((deps: D) => f(reader(deps)))
Sedangkan pada bind
, f(reader(deps))
tidak lagi menghasilkan B
, melainkan Reader<D, B>
, alias (deps: D) => B
. Sehingga untuk mendapatkan nilai B
, kita hanya perlu menyuplainya dengan (deps: D)
sekali lagi.
bind = (f, readerA) => ((deps: D) => f(reader(deps))(deps))
Type-nya sudah benar (bisa dicek sendiri dengan Typescript), artinya kita telah selesai membuat implementasi bind
pada Reader Monad! ππ
Balik ke Contoh
Kita sudah tau implementasi map
dan bind
pada Reader.
atau versi yang rada OOP dikit biar gampang nanti chaining-nya:
Biar gak scroll ke atas lagi, saya tuliskan ulang salah satu fungsi dari contoh kita di awal artikel.
debit(no: string, amount: number): (accountRepo: AccountRepo) => Account
Bisa kita lihat bagian yang saya bold, itu artinya fungsi debit
ini memiliki dependency bertipe AccountRepo
dan nilai kembaliannya bertipe Account
. Dengan Reader Monad, type signature fungsi ini bisa kita aliaskan menjadi
debit(no: string, amount: number): Reader<AccountRepo, Account>
Keduanya sama saja. Kita gak akan mengubah apapun. Yang ingin kita ubah hanya implementasi fungsi transaksiBambang
.
Kode di atas bisa kita refactor sedikit. Kita bisa lihat bahwa fungsi debit
, credit
, dan balance
tidak bergantung pada hasil komputasi sebelumnya, mereka hanya bergantung pada no
dan amount
. Saya pindahkan closure di composeK
ke masing-masing fungsi tersebut.
Bagi yang penasaran sama fungsi composeK
, bisa mampir dulu~
Dengan melihat fungsi transaksiBambang
, kita tidak lagi melihat dependency accountRepo
yang dioper secara eksplisit satu per satu. Namun, jika kita intip type signature-nya, kita baru bisa mengetahui bahwa fungsi ini memiliki dependency terhadap AccountRepo
:
const transaksiBambang: (no: string) => Reader<AccountRepo, number>
Untuk lebih mengetahui kegunaan Reader Monad dari segi komposisinya, saya berikan satu contoh lagi yang lebih simple. Misal saya punya Dependency Container seperti
selanjutnya bisa kita perhatikan semua fungsi di bawah ini adalah hasil komposisi terhadap fungsi lainnya, hanya dengan menggunakan konsep Reader Monad:
Note: semua fungsi dia atas bertipe Reader<Deps, β¦>
π
Kesimpulan
Reader Monad memungkinkan kita untuk βmenyembunyikanβ dependencies ketika melakukan komposisi sehingga kita bisa fokus kepada komposisinya, kepada mengatur business logic-nya, tanpa perlu pusing-pusing passing dependency satu per satu. Ketika kita ingin mengetahui apa saja dependency yang dibutuhkan, kita hanya perlu melihat type signature dari fungsi yang membutuhkan dependency tersebut. Sehingga bisa saya sebut Reader Monad ini ada di tengah-tengah: tidak terlalu eksplisit dan verbose, dependency ter-declare secara jelas, tidak juga melakukan βmagicβ dalam me-resolve dependencies (Yes, Iβm looking at you, Laravel).
Poin menarik lainnya yang bisa dipetik adalah bahwa ternyata sebuah fungsi bisa kita buat menjadi Monad. Seperti yang telah kita perlajari, Reader Monad ini hanyalah sebuah fungsi yang menerima 1 argumen dan mengembalikan suatu nilai: a function in its simplest form. Tapi dengan mengikuti hukum Functor dan Monad seperti yang kita lakukan bersama barusan, berubahlah dia jadi Dependency Injector π (kalo liat di sini sih, Reader Monad bisa juga banyak implementasinya).
Masih ada beberapa istilah lagi tentang Reader Monad yang sebenernya ingin saya tulis (seperti ask
dan local
), tapi kayaknya udah cukup panjang artikelnya. Kalo panjang-panjang, ntar si Bambang udah keburu nikah. Insyaallah kalo udah ngerti dasar konsep Reader ini, ask
dan local
akan sangat mudah dimengerti.
Anyway, semoga tulisan ini bermanfaat. Kalo ada bagian yang kurang tepat, mohon koreksinya. Karena saya sendiri juga baru belajar konsep ini. Sama-sama berbagi ya π. Hatur nuhun. Salaam.