Monad: Reader
This summary follows the minimum useable principle.
Readings
Monadic Semantics
Several functions depend on the same env information. In other words, they are all readers of the same environment information. Many Readers composed together by >>= or >=> to be a functional unit. The env information is being passed implicitly through the chain of computation.
type ReaderT r m a = ReaderT { runReaderT :: r -> m a }
- Function
askintroduce theenvinformation into the Reader Monad local(withReaderT) alter theenvinformation temporarily.asksconvert a function fromenvto other type to a Reader Monad.- Usually, one
askwill related to one Reader Monad that represent a function depends onenvinformation.
ask
ask :: (Monad m) => ReaderT r m r
ask = ReaderT return
-- return :: r -> m r , return in this case is defined for 'Monad m'
local
local
local
:: (r -> r) -- ^ The function to modify the environment.
-> ReaderT r m a -- ^ Computation to run in the modified environment.
-> ReaderT r m a
local = withReaderT
withReaderT
withReaderT
:: (r' -> r) -- ^ The function to modify the environment.
-> ReaderT r m a -- ^ Computation to run in the modified environment.
-> ReaderT r' m a
withReaderT f m = ReaderT $ runReaderT m . f
asks
asks
asks :: (Monad m)
=> (r -> a) -- ^ The selector function to apply to the environment.
-> ReaderT r m a
asks f = ReaderT (return . f)
{-# INLINE asks #-}
Common usage
- use
askto introduce theenvinto computation (toy models). - use
asks,Hastype class ,MonadReaderandReaderTpattern. - so we could construct functions of type
a -> Reader r bor justReader r b. (compulsory) localorwithReaderTalter environment (optional)runReaderorrunreaderTto unwrap functions. (compulsory)- Feed the
envinformation (compulsory)
Intuition:
Pass Env/Context/Configuration information through a chain of operations that depend on same set of configurations.
terms
function chain (composition):
A sequence of functions that compose.A :: a -> bB :: b -> cC :: c -> dD :: d-> e
A chain of functions above:
D . C . B . A :: a -> e
intuitation recap
- pass environment information
envthrough all components of a function chain. - every functions in this function chain use this
envor part of thisenv. Common usage include:
> ask :: introducing `env` into function chain. > local :: temporarily change `env` value or type. > asks :: swiftly turn a function of type `env -> result` into this monad. > runreaderT :: Get the functions (chain of operations), so it can be evaluated by feeding in a `env`.
Examples
0.necessary imports
-- Example One imports
import Control.Monad.Trans.Reader
import GHC.Float
-- Example Two imports
import Control.Monad.IO.Class
import Control.Monad -- for the use of kleisli arrow ( >=> )
-- Staging Example imports
import GHC.Float
import Data.Store
1.Example one
- A chain of Core functions.
Intuition:
an initial input -> a chain of functions
which inputs and outputs aligned and represented by a Reader Monad.- Every operation in this function chain shares the same
envinformatoin.- CoreInputType -> Reader Env ChainOutputType
- The
envbeing passed down through the core operation chain.
core function 1
:: Int -> Inta1 :: Int -> Reader Int Int a1 n = do env <- ask -- get Enviroment information return $ n + envcore function 2 ::
Int -> Floata2 :: Int -> Reader Int Float a2 n = do env <- ask -- get Enviroment information let fn = fromIntegral n return $ fromIntegral env / fncore function 3 ::
Float -> Doublea3 :: Float -> Reader Int Double a3 f = do env <- ask -- get Enviroment information let fn = fromIntegral env return $ float2Double $ fn * fA Chain of core functions.
- Input is a1 of type
Int - chain output is a3 of type
Double Only when input and
envbeing provided, the result can be produced.chainA :: Int -> Reader Int Double chainA n = do t1 <- a1 n -- core operaiton 1 t2 <- a2 t1 -- core operaiton 2 a3 t2 -- core operation 3alternative:
kleisli arrowchainA' :: Int -> Reader Int Double chainA' = a1 >=> a2 >=> a3
- Input is a1 of type
example output
> let p1 = chainA 10
> runReader p1 $ 2
> 0.3333333432674408
In this example env is 2 and n (the initial input) is 10 so :
t1 = 10 + 2 , t2 = 2 / 12, t3 = 2* (2 / 12) = 0.33333333
2.Example Two
- A chian of functions depend only on
Envinformation. Intuition :
Sequence operation depends only on Environment.
b1 :: (MonadIO m) => ReaderT String m Int b1 = do env <- ask -- get Environment information n <- liftIO getLine let n1 = read env n2 = read n return $ n1 + n2b2 :: (MonadIO m) => ReaderT String m Float b2 = do env <- ask -- get Environment information n <- liftIO getLine let n1 = read env n2 = read n return $ n1 / n2b3 :: (MonadIO m) => ReaderT String m Double b3 = do env <- ask -- get Enviroment information n <- liftIO getLine let n1 = read env n2 = read n return $ n1 * n2
function b1, b2, b3 depend on same
envof type StringtryB :: ReaderT String IO Float tryB = do t1 <- b1 t2 <- b2 t3 <- b3 return $ fromIntegral t1 + t2 + double2Float t3runReaderT tryB–> function that take a Env and produce the result of this composition chain.> :info tryB > tryB :: ReaderT String IO Float > let f = runReaderT tryB > :info f > f :: String -> IO Float -- Defined at <interactive>:6:5> runReaderT :: ReaderT --> function :: Env -> Result > let p = runReaderT tryB > r <- p "10" > 1 > 2 > 3 > r > 46.0
In this example env is 10 and it is of type string. Each function take another input from the commond line.
- 1+10 + 10/2 + 3*10 = 46
3.Example Three
local :: (r -> r) -> ReaderT r m a -> ReaderT r m a
withReaderT :: (r' -> r) -> ReaderT r m a -> ReaderT r' m a
- Intuition :
- local change env to the same type .
- withReaderT change env to another type .
IMPORTANT: The modification only effects temporarily. That is the reason why the name is
local.changeEnv :: String -> Float changeEnv s = read s + 100
This reader c1 is different from b1 or b2 or b3. Its env information is of type Float.
c1 :: (Monad m) => ReaderT Float m Float
c1 = do
env <- ask
return $ env * 5
tryC :: ReaderT String IO Float
tryC = do
t1 <- b1
t2 <- b2
tc1 <- withReaderT changeEnv c1
t3 <- b3
return $ fromIntegral t1 + t2 + double2Float t3 + tc1
> let p = runReaderT tryC
> r <- p "10" -- env is "10"
>1 -- b1 readin 1
>2 -- b2 readin 2 ; computation c1
>3 -- b3 readin 3
> r
-- 346.0
The result is t1+t2+t3+tc1
1+10 + 10/2 + 10*3 + (10+100)*5 = 596
- local modify the value of Env temporarily.
- withReaderT is more general, it could modify the type of Env. The definition of withReaderT guarantee the computation stays in the original
envcontext.
ReaderT r' m a
local
:: (r -> r) -- ^ The function to modify the environment.
-> ReaderT r m a -- ^ Computation to run in the modified environment.
-> ReaderT r m a
local = withReaderT
withReaderT
:: (r' -> r) -- ^ The function to modify the environment.
-> ReaderT r m a -- ^ Computation to run in the modified environment.
-> ReaderT r' m a
withReaderT f m = ReaderT $ runReaderT m . f
4.Example Four
asks :: (Monad m) => (r -> a) -> ReaderT r m a
asks f = ReaderT (return . f)
Intuition:
convert an simple function into ReaderT.
simpleFunc :: String -> Float simpleFunc s = 1000 + read stryD :: ReaderT String IO Float tryD = do t1 <- b1 t2 <- b2 tc1 <- withReaderT changeEnv c1 t3 <- b3 td1 <- asks simpleFunc return $ fromIntegral t1 + t2 + double2Float t3 + tc1 + td1> let p = runReaderT tryD > r <- p "10" >1 >2 >3 > r >1606.0
This example is: t1 + t2 + t3 + tc1 + td1
1+10 + 10/2 + 10*3 + 110 * 5 + (10+1000)= 1606
Notes
- Given a function of type
:: r -> a, we could useasksto convert it monadReaderT r m a. However, it is not certainly applicable the other way around.
It could be impossible to factorize a function of typeReaderT r m aas combinations ofasksandf.- The implementation of
asksisasks f = ReaderT (return . f) - In
ReaderT r m a, ifmisEitherorIO, there will be no hope forreturn . fto produceComputational ContextInformation ofm. - In this case,
musually related to more than one Type and each Type contains more than one possible value.
- The implementation of
5.Complex Example : Staging
Business logic :
Core functions:
f1 :: a -> bf2 :: b -> cf3 :: c -> df4 :: d -> ef5 :: e -> f
chainF :: a -> f
chainF = f5 . f4 . f3 . f2 . f1Core Functions depends on
Configinformation. So :f1 :: a -> e -> bf2 :: b -> e -> cf3 :: c -> e -> df4 :: d -> e -> ef5 :: e -> e -> f
chainF :: e -> a -> fWe could rewrite
f1tof5as: -f1 :: a -> Reader e b-f2 :: b -> Reader e c-f3 :: c -> Reader e d-f4 :: d -> Reader e e-f5 :: e -> Reader e fchainF :: a -> Reader e fchainF a = do e <- ask b <- f1 a c <- f1 b d <- f1 c e <- f1 d f <- f1 e return for
chainF = f1 >=> f2 >=> f3 >=> f4 >=> f5The computation of each core function could be very complicated and time consuming, so I would like to save the result of every step on the disk. If corresponding
configdoesn’t change, the next time whenchainFis running we can simple deserialize existed result and provide to the following functions.
4. ReaderT implementations:
The
Envcontains information for a chain of computations, functions for serialization and deserialization. ThisEnvcould be initialized fromConifginformation.type Serializer = forall a. (Store a) => a -> FilePath -> IO () type Deserializer = forall a. (Store a) => FilePath -> IO a data Env = Env { sf1Env :: String , sf2Env :: Int , sf3Env :: Float , sf4Env :: Double , sf5Env :: Int , serializer :: Serializer , deserializer :: Deserializer }StageCore function relies on
envinformationtype StageCore a = ReaderT Env IO a
Functions that fully or partially depends on
envinformation.-- sf1 -- :: (Monad m) -- => ReaderT Env m Int sf1 :: StageCore Int sf1 = do env <- ask return $ length . sf1Env $ envsf2 :: Int -> StageCore Float sf2 arg2 = do env <- ask return $ fromIntegral $ sf2Env env + arg2sf3 :: Float -> StageCore Double sf3 arg3 = do env <- ask return $ float2Double $ sf3Env env + arg3sf4 :: Double -> StageCore String sf4 arg4 = do env <- ask return $ show $ sf4Env env + arg4sf5 :: StageCore Int sf5 = do env <- ask return $ 100 + sf5Env envcoreChain :: () -> ReaderT Env IO Int coreChain = (\_ -> sf1) >=> sf2 >=> sf3 >=> sf4 >=> (\_ -> sf5)Now we would like to:
- Serialize the output of each core functions to the disk.
- If env information is new , we do the calculation.
- If previous calculation is new, we do this calculation.
- otherwise, we deserialize what we saved on the disk.
we need to name each stage/computation
type StageName = Stringinformation need for core function
a -> bnow need to embellished with aBooleantype to indicate the serialization status.
type Core a = (a, Bool)We use Data.Store to serialize output of each core function
StageName and Env defines this boolen valuecheckSerialization :: StageName -> StageCore (Core FilePath) checkSerialization = undefinedFunction
stagingtake:- a
StageNameto identify this computation. - a core function of type
a -> StageCore b. - produce a new function ` Core a -> StageCore (Core b)
This would be make more sense, - embellish input/output of the original core-function with the Bool information
(a, Bool) -> StageCore (b,Bool) == Core a -> StageCore (Core b) - this bool information is being used for helping following operation to decide whether to do the computation or just desrialize from dist.
a
StageNamebeing used to identify stage and influence the Boolean information.staging :: (Store b) => StageName -> (a -> StageCore b) -> Core a -> StageCore (Core b) staging stageName coreFunc (coreInput, preStatus) = do env <- ask let savetoDisk = serializer env readDisk = deserializer env (serializationPath, done) <- checkSerialization stageName if done && preStatus then do ds <- liftIO $ readDisk serializationPath return (ds, True) else do coreOutput <- coreFunc coreInput liftIO $ savetoDisk coreOutput serializationPath return (coreOutput, False)
- a
Finally,
Stagingcould be used like this:stageChain = staging "stageFunction_1" (\_ -> sf1) >=> staging "stageFunction_2" sf2 >=> staging "stageFunction_3" sf3 >=> staging "stageFunction_4" sf4 >=> staging "stageFunction_5" (\_ -> sf5)
6. ReaderT Design Pattern
- The famous 23 design pattern is almost perfect except for the suspicious number
23.- This number
23is not as convincing as the number42.- With highly abstracted expression ability, the design patterns of Haskell iImplementation would be more like a personal preference or a hobby rather than some task specific best practices.
Readings
- Monad Reader
- fp complete: ReaderT (compulsory)
- haskell in production
Desire
- Want to know what is this function about from the type signature.
issues and solutions
Information in the function type signature:
- Obvious: core function is from
DoubletoString - Blur: The meaning of StageCore. Needs reference of
StageCore No: which part of
envaffects this function.sf4 :: Double -> StageCore String sf4 arg4 = do env <- ask return $ show $ sf4Env env + arg4
- Obvious: core function is from
Use
Hastype class to constraint only necessary information to be used inenvtype NewStage r a = ReaderT r IO a class HasEnv4 a where getSF4env :: a -> Double
NewStage Double String enabling the use of Double in test. So we don’t need to construct a meaningless env .
instance HasEnv4 Double where
getSF4env = id
instance HasEnv4 Env where
getSF4env = sf4Env
sf4N
:: (HasEnv4 r)
=> Double -> NewStage r String
sf4N arg4 = do
env <- ask
return $ show $ getSF4env env + arg4
Example
- The above
Complex exampleis the main body of ReaderT design pattern. - Initialize
Envusing configurator. Each computation in the computation chain only relies on part of
Env. Fix this by usingHastypeclass.data Env = Env { sf1Env :: String --being used by sf1 only , sf2Env :: Int --being used by sf2 only , sf3Env :: Float --being used by sf3 only , sf4Env :: Double --being used by sf4 only , sf5Env :: Int --being used by sf5 only , serializer :: Serializer , deserializer :: Deserializer }
1. Define type class
--TypeClass for retrieving sf1Env from Env
class HasEnv1 a where
getSF1env :: a -> String
instance HasEnv1 String where
getSF1env = id
instance HasEnv1 Env where
getSF1env = sf1Env
-- TypeClass for retrieving sf2Env from Env
class HasEnv2 a where
getSF2env :: a -> Int
instance HasEnv2 Int where
getSF2env = id
instance HasEnv2 Env where
getSF2env = sf2Env
-- TypeClass for retrieving sf3Env from Env
class HasEnv3 a where
getSF3env :: a -> Float
instance HasEnv3 Float where
getSF3env = id
instance HasEnv3 Env where
getSF3env = sf3Env
-- TypeClass for retrieving sf4Env from Env
class HasEnv4 a where
getSF4env :: a -> Double
instance HasEnv4 Double where
getSF4env = id
instance HasEnv4 Env where
getSF4env = sf4Env
-- TypeClass for retrieving sf5Env from Env
class HasEnv5 a where
getSF5env :: a -> Int
instance HasEnv5 Int where
getSF5env = id
instance HasEnv5 Env where
getSF5env = sf5Env
Two instances were declared, with the help of the first function getEnv = id, we could test function depends on this typeclass easily (defined as follow).
2.define corresponding functions
-- | Then function sf1 to sf5 could rewrite as follow
type NewStage r a = ReaderT r IO a
sf1N
:: (HasEnv1 r)
=> NewStage r Int
sf1N = do
env <- ask
return $length . getSF1env $ env
sf2N
:: (HasEnv2 r)
=> Int -> NewStage r Float
sf2N arg2 = do
env <- ask
return $ fromIntegral $ getSF2env env + arg2
sf3N
:: (HasEnv3 r)
=> Float -> NewStage r Double
sf3N arg3 = do
env <- ask
return $ float2Double $ getSF3env env + arg3
sf4N
:: (HasEnv4 r)
=> Double -> NewStage r String
sf4N arg4 = do
env <- ask
return $ show $ getSF4env env + arg4
sf5N
:: (HasEnv5 r)
=> NewStage r Int
sf5N = do
env <- ask
return $ 100 + getSF5env env
In this case:
1. function associate with meaningful type signatures
2. It can be test very easily.
3. The type of the core computation could stay the same as before, but we updated the participant component with more flexibility.
3. new computation chain
Type signature stays the same as before
NewStage Env Int = StageCore Int = ReaderT Env IO Int
newCoreChain :: () -> NewStage Env Int
newCoreChain = (\_ -> sf1N) >=> sf2N >=> sf3N >=> sf4N >=> (\_ -> sf5N)
-- newCoreChain = (\_ -> sf1) >=> sf2 >=> sf3 >=> sf4 >=> (\_ -> sf5)
4. Refactor using Lens
TODO