Akses Global Values dengan Reader Monad

Mar 21, 2020 12:46 ยท 849 words ยท 4 minute read #purescript #haskell #types #monad #functionalprogramming

Akses Global Values dengan Reader Monad
Image by Jill Wellington from Pixabay

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:

PureScript
Reader env a

dimana 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:

PureScript
type Env = {
  baseUrl :: String
}

port :: Reader Env Int
port = pure 4005

Type 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:

PureScript
runReader :: Reader env a -> env -> a

env :: Env
env = { baseUrl: "http://localhost" }

result :: Int
result = runReader port env

runReader 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!

PureScript
insertPort :: Reader Env String
insertPort = do
  { baseUrl } <- ask
  pure $ baseUrl <> ":4005"

env :: Env
env = { baseUrl: "http://localhost" }

result :: Int
result = runReader insertPort env
-- "http://localhost:4005"

Contoh yang lebih real world:

PureScript
fetchAuthedUser :: Reader Env User
fetchAuthedUser = 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 Data
fetchAllData = 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.

PureScript
type Env = {
  currentUser :: Ref (Maybe User),
  baseUrl :: String
}

main :: Effect Unit
main = do
  -- Jalankan dengan nilai awal kosong
  currentUser <- Ref.new Nothing

  let env :: Env = {
    currentUser,
    baseUrl: "https://..."
  }

  runApp env

Nanti di bagian aplikasi lain, ketika user telah ter-autentikasi, barulah currentUser dapat di-update dan dibaca oleh function lain

PureScript
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 HTML
navbar = 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.

Edit on