Skip to content

Instantly share code, notes, and snippets.

@ecthiender
Created July 21, 2021 07:40
Show Gist options
  • Save ecthiender/5484b67c1251a20af00cf85f9cdead70 to your computer and use it in GitHub Desktop.
Save ecthiender/5484b67c1251a20af00cf85f9cdead70 to your computer and use it in GitHub Desktop.
Example of how to use Reader monad
{-# LANGUAGE FlexibleContexts #-}
module Main where
import Control.Applicative (liftA2)
import Control.Monad.Reader (MonadIO, MonadReader, ReaderT, ask,
runReaderT)
{- | Let's say we have a deeply nested call stack. There are many functions
calling other functions.
In the below example, the call stack looks like -
main -> runApp -> doSomethingSpecial -> doFooAndBar -> doFoo
And other than doFoo, the config parameter is not used by any other function
call. They just plumb through the config
-}
main :: IO ()
main = do
config <- readConfig
res <- runApp config
putStrLn $ "Final result: " ++ res
data Config = Config
{ configFoo :: Int
, configBar :: String
} deriving Show
readConfig :: IO Config
readConfig = undefined
runApp :: Config -> IO String
runApp config = do
doSomethingSpecial config
doSomethingSpecial :: Config -> IO String
doSomethingSpecial config = do
r1 <- doFooAndBar config
r2 <- doBar config
pure $ r1 ++ r2
doFooAndBar :: Config -> IO String
doFooAndBar c = liftA2 (++) (doFoo c) (doBar c)
doFoo :: Config -> IO String
doFoo c = pure $ show (configFoo c + 1)
doBar :: Config -> IO String
doBar = undefined
{- | We can use the Reader (ReaderT) monad pattern, to refactor this. So all
function calls operate with a type `ReaderT ....` but none of them have to
explictly pass down the parameter
-}
main' :: IO ()
main' = do
config <- readConfig
res <- runReaderT runApp' config
putStrLn $ "Final result: " ++ res
runApp' :: ReaderT Config IO String
runApp' = doSomethingSpecial'
doSomethingSpecial' :: ReaderT Config IO String
doSomethingSpecial' = do
r1 <- doFooAndBar'
r2 <- doBar'
pure $ r1 ++ r2
doFooAndBar' :: ReaderT Config IO String
doFooAndBar' = liftA2 (++) doFoo' doBar'
doFoo' :: ReaderT Config IO String
doFoo' = do
config <- ask
pure $ show (configFoo config + 1)
doBar' :: ReaderT Config IO String
doBar' = undefined
{- | Then to make our functions more flexible (polymorphic), we can take away the
concrete `ReaderT` monad and just use typeclass constraints. Interestingly,
below, nothing except the type signature changes! All implementations are
exactly like above!
-}
main'' :: IO ()
main'' = do
config <- readConfig
res <- runReaderT runApp'' config
putStrLn $ "Final result: " ++ res
runApp'' :: (MonadReader Config m, MonadIO m) => m String -- You can keep this type or fix this to a more concrete type like `ReaderT Config IO String`
runApp'' = doSomethingSpecial''
doSomethingSpecial'' :: (MonadReader Config m, MonadIO m) => m String
doSomethingSpecial'' = do
r1 <- doFooAndBar''
r2 <- doBar''
pure $ r1 ++ r2
doFooAndBar'' :: (MonadReader Config m, MonadIO m) => m String
doFooAndBar'' = liftA2 (++) doFoo'' doBar''
doFoo'' :: (MonadReader Config m, MonadIO m) => m String
doFoo'' = do
config <- ask
pure $ show (configFoo config + 1)
doBar'' :: (MonadReader Config m, MonadIO m) => m String
doBar'' = undefined
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment