Sembunyikan State-mu dengan State Monad
Ceritanya lagi buat aplikasi bank kecil-kecilan.
withdraw :: Int -> Intwithdraw amount = amountArgument function withdraw adalah jumlah yang ingin kita ambil dan nilai kembaliannya kita asumsikan sebagai uang yang keluar dari mesin ATM.
Namun function tersebut kurang real world. Untuk melakukan penarikan uang, mesin harus menghitung beberapa faktor yang telah ditentukan oleh pihak bank seperti jumlah penarikan, biaya penarikan, saldo, dll. Sedangkan tak ada pengecekan apapun di function tersebut. Mungkin harus kita buat versi yang lebih baik.
type Amount = Inttype Balance = Int
withdraw :: Amount -> Balance -> Tuple (Maybe Amount) Balancewithdraw amount balance = let minBalance = 50 in let afterBalance = balance - amount in
if afterBalance >= minBalance then Just amount else NothingSekarang function withdraw akan cek terlebih dahulu apakah sisa saldo setelah penarikan masih lebih dari 50. Jika demikian, penarikan berhasil. Namun sayangnya sisa saldo setelah penarikan tidak berubah sama sekali. Mungkin perlu diiterasi lagi supaya mengembalikan dua hal sekaligus: jumlah yang keluar dari ATM, dan hasil saldo akhir setelah transaksi.
type Amount = Inttype Balance = Int
withdraw :: Amount -> Balance -> Tuple (Maybe Amount) Balancewithdraw amount balance = let minBalance = 50 in let afterBalance = balance - amount in
if afterBalance >= minBalance then Tuple (Just amount) afterBalance else Tuple Nothing balanceDengan begini, nilai balance juga ikut dikomputasi setiap kali melakukan penarikan dan hasilnya dikembalikan ke caller.
transactions :: Int -> Stringtransactions balance = let (Tuple _ balance2) = withdraw 10 balance in let (Tuple mbAmount balance3) = withdraw 5 balance2 in
case mbAmount of Just _ -> "Saldo terakhir Anda: " <> show balance3 Nothing -> "Kismin lu!"
λ> transactions 100"Saldo terakhir Anda: 85"
λ> transactions 10"Kismin lu!"Pattern yang langsung terlihat dari penggunaan fungsi withdraw adalah nilai balance yang dioper-oper secara eksplisit dari satu function ke function lainnya, yang nilainya berpotensi berubah di setiap pemanggilan withdraw. Nilai yang berubah-ubah ini bisa kita namakan dengan State.
Sehingga, dalam domain studi kasus “bank” ini, saldo atau balance adalah State yang harus kita maintain.
Balik ke permasalahan code, walaupun dalam banyak hal code mungkin akan lebih mudah dicerna dengan explicit passing, namun dalam hal ini akan sangat tidak readable bila harus terus melakukan destructuring Tuple dan menyuplai State ke function berikutnya.
State monad bisa membantu menyembunyikannya.
Definisi State Monad
type State s a = s -> Tuple a sSebetulnya function withdraw sudah menyerupai pattern State monad. Perhatikan type signature-nya dan fokus pada Balance, karena ia adalah state yang ingin kita maintain.
type Amount = Inttype Balance = Int
withdraw :: Amount -> Balance -> Tuple (Maybe Amount) Balance-- becomeswithdraw :: Amount -> State Balance (Maybe Amount)Jadi sebenarnya State monad hanyalah sebuah function yang mengambil state dan mengembalikan state berikutnya, dibarengi dengan intermediate value (biasanya berupa hasil komputasi yang bergantung pada state).
state -> Tuple intermediateValue nextStateRefactor
Mari refactor function withdraw menggunakan State monad.
type Amount = Inttype Balance = Int
withdraw :: Amount -> State Balance (Maybe Amount)withdraw amount = do balance <- get
let minBalance = 50 let afterBalance = balance - amount
if afterBalance >= minBalance then do put (balance - amount) pure $ Just amount else pure NothingAda dua method State monad yang muncul di sini: get yang mengambil nilai state terbaru, dan put yang meng-overwrite nilai state.
Dengan style ini, function transactions menjadi lebih singkat dan kita tak perlu lagi passing state secara eksplisit.
type Amount = Inttype Balance = Int
deposit :: Amount -> State Balance Unitdeposit amount = modify (\s -> s + amount)-- `modify` sama seperti `put`, namun menerima callback
transactions :: State Balance Stringtransactions = do deposit 8 _ <- withdraw 10 mbAmnt <- withdraw 5 balance <- get pure $ case mbAmnt of Just _ -> "Saldo terakhir Anda: " <> show balance Nothing -> "Kismin lu!"Dan untuk menjalankannya, kita hanya perlu memanggil runState (atau evalState atau execState, tergantung kebutuhan) dan menyuplainya dengan initial state.
initialBalance = 100
λ> runState transactions initialBalanceTuple "Saldo terakhir Anda: 93" 93
λ> evalState transactions initialBalance"Saldo terakhir Anda: 93"
λ> execState transactions initialBalance93Proses modifikasi dan passing state ini hanya terjadi di dalam operasi monad (ketika memanggil runState), menjaga program tetap pure dan bebas dari side effect. Mungkin ini salah satu perbedaan yang cukup mencolok dibandingkan dengan pendekatan OOP yang menggunakan class properties sebagai state variable. Atau pada pendekatan prosedural yang memanfaatkan mutable variable untuk menyimpan perubahan state.
All in all, State monad memberikan jalan alternatif yang pure untuk melakukan komputasi yang “stateful”. Menambahkan state pada function a -> b cukup dengan mengubahnya menjadi a -> State s b, dan kita sudah mendapatkan function get, put dan modify secara gratis.
Semoga bermanfaat.