不知道讀者有沒有想過,如果今天我想要把每一步 monadic 操作的過程都有 log 紀錄方便我們 debug 的話要怎麼做?(用 print
大法也不是不行)
Writer
可以協助我們達成這方面的事情,我們可以利用它將每次計算的額外結果記錄起來並帶到下一次計算。
newtype Writer w a = Writer { runWriter :: (a, w) }
instance (Monoid w) => Monad (Writer w) where
return x = Writer (x, mempty)
(Writer (x,v)) >>= f = let (Writer (y, v')) = f x in Writer (y, v `mappend` v')
首先這邊利用 newtype
封裝了這個型別也就是 (a,w)
這種形式,且提供 runWriter
讓我們取值。
它對於 Monad
實作說明了, m
(也就是通常拿來做 log 的部分)要是 Monoid
,至於為什麼要是 Monoid
呢?因為 monoid 的 identity 的特性所以我們可以將 return x
(或者想像成預設的 context )變成 Writer (x, mempty)
,這樣子我們就能確保之後不管是什麼值用 mappend
進行運算,結果都會是我們傳進來的值。
而 >>=
則是利用 pattern matching 將 f x
計算的結果把 y
, v'
取出來並將 y
以及 v' mappend v'
,塞回去 context 。
我們先來寫一個小 function 來表示一個 Int
被裝進去 Writer
後要有一個 log
logNumber :: Int -> Writer [String] Int
logNumber x = writer (x, ["Got number: " ++ show x])
writer
是Writer
所提供的 function ,當然也可以直接用return x
來 wrap 後,再添加 log 來達成一樣的效果
然後寫計算這種 monadic value 的 function
addWithLog :: Writer [String] Int -> Writer [String] Int -> Writer [String] Int
addWithLog a b = do
x <- a
y <- b
tell ["added "++ show x ++ " and " ++ show y]
return (x + y)
這邊的程式碼就蠻簡單的,就單純的把 a
及 b
這兩個 monadic value 從 context 取出來後,利用 tell
添加 log ,然後再用 return
包裝一次 context,至於 tell
簡單來說就是一個添加 log 的 function 。
我們來實際跑跑看
print $ runWriter $ addWithLog (logNumber 10) (logNumber 1)
-- (11,["Got number: 10","Got number: 1","added 10 and 1"])
print $ runWriter $ addWithLog (logNumber 1) $ addWithLog (logNumber 2) (logNumber 3)
-- (6,["Got number: 1","Got number: 2","Got number: 3","added 2 and 3","added 1 and 5"])
結果如我們預期的那樣,我們每次獲得數字以及運算的結果都有寫進去 Writer
裡。
今天的程式碼
https://github.com/toddLiao469469/30days-for-haskell