Akses Global Values dengan Reader Monad
Sampai beberapa tahun yang lalu (dan mungkin masih sampai sekarang), komunitas React sempat dibuat hype dengan adanya Redux sebagai state management library. Walaupun pada kenyataannya banyak juga yang hanya menggunakan Redux sebagai wadah untuk global/shared state mereka, dalihnya sih agar terhindar dari props drilling atau menyuplai props secara eksplisit ke setiap level component tree.
Lalu muncul React Context, yang tujuan utamanya persis seperti yang barusan: agar developer terhindar dari props drilling. Dikutip dari dokumentasi resmi React:
Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.
Artikel ini saya tulis lebih karena teringatnya saya pada Reader Monad sebagai salah satu cara untuk menyimpan dan mengatur global values agar deep-nested functions tetap punya akses tanpa props drilling. Persis seperti React Context (bedanya Reader type-safe). Dan saya akan menjelaskannya dengan Purescript.
Apa itu Reader Monad
Seperti yang telah dijelaskan sebelumnya, Reader Monad berfungsi sebagai wadah penyimpanan suatu shared value. Shared value tersebut bisa berupa object connection yang nantinya dibutuhkan oleh setiap function yang menjalankan database query. Bisa juga berupa kumpulan nilai environment (configuration) yang biasa “disimpan” di file .env.
Definisi Reader Monad sangat simple:
Reader env adimana env adalah shared value kita dan a adalah nilai yang dihasilkan dari shared value tersebut, mirip sebuah function env -> a. Dan memang sejatinya Reader Monad adalah function env -> a 😄. Tapi pembahasan function sebagai Reader kita kesampingkan dulu.
Karena Reader (memiliki instance Monad), kita dapat memanggilnya seperti ini:
type Env = { baseUrl :: String}
port :: Reader Env Intport = pure 4005Type signature di atas mendeskripsikan bahwa kita telah membuat sebuah Reader yang menerima sebuah object Env dan menghasilkan Int. Lalu bagaimana cara menyuplai object Env dan mendapatkan nilai 4005? Dengan function runReader:
runReader :: Reader env a -> env -> a
env :: Envenv = { baseUrl: "http://localhost" }
result :: Intresult = runReader port envrunReader menerima Reader di argument pertamanya lalu disuplai dengan shared value env di argument kedua sehingga menghasilkan nilai a. Namun sampai contoh barusan kita belum juga menggunakan Env atau shared value yang kita suplai. Lalu bagaimana cara mendapatkannya? Dengan method ask!
insertPort :: Reader Env StringinsertPort = do { baseUrl } <- ask pure $ baseUrl <> ":4005"
env :: Envenv = { baseUrl: "http://localhost" }
result :: Intresult = runReader insertPort env-- "http://localhost:4005"Contoh yang lebih real world:
fetchAuthedUser :: Reader Env UserfetchAuthedUser = do env <- ask user <- Ajax.get (env.baseUrl <> "/my-details") pure user
fetchArticles :: Reader Env (Array Article)fetchArticles = do env <- ask user <- fetchAuthedUser articles <- Ajax.get (env.baseUrl <> "/article?userId=" <> user.id) pure articles
fetchAllData :: Reader Env DatafetchAllData = do articles <- fetchArticles tags <- fetchTags ... ... pure someData
allData = runReader fetchAllData { baseUrl: "http://localhost:4005" }Ketika ask dipanggil, ia akan mengembalikan environment yang nantinya disuplai. Jadi kalau ingin mendapatkan environment, ask for it 😉.
Kurang lebih inilah yang dimaksud dengan Reader Monad beserta penggunaan dasarnya. Lihat bagaimana dalam pemanggilan function fetchAuthedUser kita tidak pernah benar-benar secara eksplisit melempar object Env ke function argument-nya. Begitu pula dalam pemanggilan fetchArticles, tidak ada explicit passing object Env. Semua “disembunyikan” lewat Reader.
Update Global Value?
Lumrahnya penggunaan Reader Monad hanyalah untuk dibaca value-nya, sesuai dengan namanya, Reader. Namun bukan berarti Reader tidak bisa diperlakukan seperti global setter. Kita tetap bisa meng-update environment yang tersimpan di Reader. Dengan sedikit hack: Ref.
Ref adalah struktur data yang mutable di Purescript. Segala operasi yang berkaitan dengannya harus dijalankan di bawah payung Effect Monad, karena memang nature-nya yang tidak pure.
Contoh kecil adalah ketika aplikasi baru diakses lewat browser dan user belum melakukan login. Sedangkan object user ini perlu diakses di banyak tempat. Kita tetap bisa menempatkan object user ini sebagai shared value di Reader.
type Env = { currentUser :: Ref (Maybe User), baseUrl :: String}
main :: Effect Unitmain = do -- Jalankan dengan nilai awal kosong currentUser <- Ref.new Nothing
let env :: Env = { currentUser, baseUrl: "https://..." }
runApp envNanti di bagian aplikasi lain, ketika user telah ter-autentikasi, barulah currentUser dapat di-update dan dibaca oleh function lain
authenticate :: ∀ m. MonadAsk Env m => MonadEffect m => m (Maybe User)authenticate = do env <- ask case fetchProfile env.baseUrl of Left _ -> pure Nothing Right user -> do liftEffect $ Ref.write (Just user) env.currentUser pure (Just user)
navbar :: ∀ m. MonadAsk Env m => MonadEffect m => m HTMLnavbar = do env <- ask mbUser <- liftEffect $ Ref.read env.currentUser case mbUser of Nothing -> H.button_ [H.text "Login"] Just user -> H.text ("Welcome, " <> user.name)Wrap up
Kesimpulannya: Reder Monad sangat cocok untuk meneruskan informasi/value secara implisit untuk menghasilkan sebuah komputasi. Setiap kali kita memiliki sebuah value yang dibutuhkan di banyak tempat namun ingin menghasilkan hasil komputasi yang berbeda-beda, maka Reader Monad bisa menjadi salah satu solusi.
Saya harap tulisan ini dapat membantu dalam memahami motivasi di balik Reader Monad beserta penggunaannya. Terima kasih.