Reader monad

程式中時常有許多函數是需要去使用共用的常數(shared environment),常見的像是設定檔(config)之類的東西。

此時函數的參數就是這些使用到的如 config,我們必須在 type signature 寫出需要的參數型別,此外在使用時必須顯式的寫出要傳入的東西

1
2
3
4
5
6
7
8
9
-- 假設此時的 configs 中有 name age weight height 等等
myFunc1 :: Name -> Age -> ...
myFunc2 :: Name ->...
myFunc3 :: Weight -> Height -> ...
-- ...
myFunc1 name age
myFunc2 name
myFunc3 weight height
-- ...

這樣的壞處很明顯就是我們必須重複寫很多東西。
要解決這問題的一個直覺想法就是,讓這些函數所在的 scope,可以自由地取得共用的資料,如此一來就不需由我們手動傳入了。
而在 functional programming 中,就是使用函數去達成。只要將這些函數做良好的定義,並塞到一個更大的函數裡,將共享資源傳入這個大函數中,裡頭的小函數們就都可以去取得了。

Reader monad 就是可以幫我們做這件事情的工具,因此他又有另外的名稱,Environment monad。
而最簡單的 Reader monad 其實就是一個函數的新名稱罷了。


範例

這裡用一段沒什麼用處的程式碼來舉例。
這裡的函數用返回執行別用Maybe,只是為了要用do notation而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type Name = String
type Age = Int
type Height = Double
type Weight = Double
type Result = (String, String, String, Double)
revName :: Name -> Maybe String
revName n = Just $ "Reverse Name: " ++ reverse n
nameAndAge :: Name -> Age -> Maybe String
nameAndAge n a = Just $ "Name & Age: " ++ n ++ " " ++ show a
weightAndHeight :: Weight -> Height -> Maybe String
weightAndHeight w h = Just $ "Weight & Height: " ++ show w ++ " " ++ show h
calBmi :: Height -> Weight -> Maybe Double
calBmi h w = Just $ w / (h / 100)^2
go =
let
name = "Tom"
age = 20
height = 180
weight = 100
in
do
na <- nameAndAge name age
n <- revName name
wh <- weightAndHeight weight height
b <- calBmi height weight
return (na, n, wh, b)

可以發現 nameAndAge, revName, weightAndHeight, calBmi等函數,我們需要定義、傳地重複的東西進去,使程式碼看起來較為繁冗。
而他們用到的這些 input 就可以將其想像成 shared environment 這種在程式執行時並不會去改變且很多地方都會用到的東西。


定義 Reader monad

1
newtype Reader r a = Reader { runReader :: r -> a }

也就是說,Reader 就是包裹了一個函數 r -> a ,其中 r 就是我們要傳入的共用資料,有些地方也會用e來表示(environment);a 表示吃進 r 後,會回傳的東西是什麼。

再來我們可以自己定義他一系列的Functor Applicative Monad instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
instance Functor (Reader r) where
fmap :: (a -> b) -> Reader r a -> Reader r b
fmap f (Reader ra) = Reader $ f . ra
instance Applicative (Reader r) where
pure :: a -> Reader r a
pure a = Reader $ const a
(<*>) :: Reader r (a -> b) -> Reader r a -> Reader r b
(Reader rab) <*> (Reader ra) =
Reader $ \r -> rab r (ra r)
instance Monad (Reader r) where
return = pure
(>>=) :: Reader r a -> (a -> Reader r b) -> Reader r b
(Reader ra) >>= aRb =
Reader $ \r -> runReader (aRb (ra r)) r

可以看到這裡的 structure 是 Reader r 也就是 (->) r (function type) 這部分。而且在實作當中可以看到,r是保持著原來的值被傳遞,並沒有被做其他transform,因此可以確保 Reader monad 可以拿到相同的r

對照 (->) r原始碼中定義的Functor Applicative Monad instance,可以發現基本上是一樣的,只是多了 Reader 這一層包裝。

若將型別定義中的 Reader r(->) r 替換,可以看到更明顯的結果

1
2
3
4
5
6
7
8
9
10
11
fmap :: (a -> b) -> Reader r a -> Reader r b
= (a -> b) -> (->) r a -> (->) r b
= (a -> b) -> (r -> a) -> (r -> b)
(<*>) :: Reader r (a -> b) -> Reader r a -> Reader r b
= (->) r (a -> b) -> (->) r a -> (->) r b
= (r -> a -> b) -> (r -> a) -> (r -> b)
(>>=) :: Reader r a -> (a -> Reader r b) -> Reader r b
= (->) r a -> (a -> ((->) r b)) -> (->) r b
= (r -> a) -> (a -> (r -> b) -> (r -> b)

所以其實 Reader monad 的概念,就是跟 function type (->) r 是一樣的。


Methods

使用了newtype Reader將 function 包裝後,還沒有什麼用。
還需要一些小工具使我們可以方便地拿到想要的資料,askasks以及local

1
2
3
4
5
6
7
-- 取得 shared environment
ask :: Reader a a
ask = Reader id
-- = Reader $ \r -> r
asks :: (r -> a) -> Reader r a
asks = Reader

ask就是一個id,也就是傳什麼就吐一樣的東西回來。所以就是可以拿到Reader r a中的r

1
2
3
4
ex :: Reader Config String
ex = do
config <- ask
-- ...

asks的參數是一個函數,這個函數的作用就類似 selector,用來塞選取得在 shared environment 中想要的資料。

1
2
3
4
ex :: Reader Config String
ex = do
n <- asks $ lookup "name"
-- ...

local是用來修改 Reader content,但他不是修改全域的內容,而是只有在local scope 裡面的 Reader Monad 才會被影響。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local :: (r -> r) -> Reader r a -> Reader r a
local f (Reader ra) = Reader $ \r -> ra (f r)
-- example
ex :: Reader Int (Int, Int)
ex = do
i <- local (+1) $ do
i <- ask
return i
j <- ask
return (i, j)
> runReader ex 10
(11, 10)

ex的執行範例可以看到,i也就是經過local後 Reader content 的確是被修改了,但是用ask拿到的 Reader content j,還是為 10 沒有改變。
所以local的作用是區域修改而不會影響到其 scope 外的地方,所以總的來說其實 Reader content並沒有發生改變。


範例改寫

原本的範例程式可以用 Reader monad 如此改寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
type Result = (String, String, String, Double)
data Config = Config { name :: String
, age :: Int
, height :: Double
, weight :: Double
}
revName :: Reader Config String
revName = do
n <- asks name
return $ "Reverse Name: " ++ reverse n
nameAndAge :: Reader Config String
nameAndAge = do
n <- asks name
a <- asks age
return $ "Name & Age: " ++ n ++ " " ++ show a
weightAndHeight :: Reader Config String
weightAndHeight = do
w <- asks weight
h <- asks height
return $ "Weight & Height: " ++ show w ++ " " ++ show h
calBmi :: Reader Config Double
calBmi = do
w <- asks weight
h <- asks height
return $ w / (h / 100)^2
go :: Reader Config Result
go = do
na <- nameAndAge
n <- revName
wh <- weightAndHeight
b <- calBmi
return (na, n, wh, b)
showResult = runReader go $ Config {name = "WOW", age = 20, height = 190, weight = 100}

主要的差別在於,每個函數的 type signature 是一致的 Reader Config [回傳型別],大家是共享一份 config。
各函數在其內部自行定義要取得什麼資料,如此一來在使用的時候就不需要顯示的傳遞進去了。


結語

一般情況在使用 Reader Monad 時並不會自行去定義,可能會用一些 library 像 mtltransformers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Control.Monad.Reader
data MyContext = MyContext
{ foo :: String
, bar :: Int
} deriving (Show)
computation :: Reader MyContext (Maybe String)
computation = do
n <- asks bar
x <- asks foo
if n > 0
then return (Just x)
else return Nothing
ex1 :: Maybe String
ex1 = runReader computation $ MyContext "hello" 1
ex2 :: Maybe String
ex2 = runReader computation $ MyContext "haskell" 0

他們同樣也都提供了基本的像是ask, asks, local,可以去看看這些 library 是如何實作的,藉此來學習。

此外通常看到或是使用時,因為會需搭配其他的 Monad 一起使用,並不會用Reader而是使用ReaderTT表示 transformer。


參考資料及更多資源

  1. mtl
  2. transformers
  3. Three Useful Monads - adit.io
  4. What I Wish I Knew When Learning Haskell 2.3 ( Stephen Diehl )
0%