Learn You a Haskell for Great Good 读书笔记 14

More Monad

Writer

我们想在程序运行的时候打log怎么办? 用(a, String)去记录结果,那如何把一连串的log连起来而不是只保留一步的log呢? 用Monad, 然后用++把String连起来:

1
2
applyLog :: (a, String) -> (a -> (b, String)) -> (b, String)
applyLog (x, log) f = let (y, newLog) = f x in (y, log ++ newLog)

这里对String可以作进一步的抽象。String其实就是[Char],而(++)对应是Monoid里的mappend,那String可以抽象成Monoid,即任何可以foldable的类型。

1
2
applyLog :: (Monoid m) => (a, m) -> (a -> (b, m)) -> (b, m)
applyLog (x, log) f = let (y, newLog) = f x in (y, log `mappend` newLog)

当我们做了这一层的抽象后,显然log就不能代表这个monad的全部功能了,因为monoid可不止List, 还有Sum等,于是我们有了Writer:

1
2
3
4
newType Writer w a = Writer { runWriter :: (a, w) }
instance (Monoid m) => 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')

注意由上推出的>>变得有意思了,monad包含的值会取运算符后面的monad的值,而monad中tuple包含的monoid则是经过mappend组合在了一起:

1
2
3
(Writer (x, v)) >> (Writer (y, v')) = Writer (x, v) >>= \n -> Writer (y, v')
= let Writer (y, v') in Writer (y, v `mappend` v')
= Writer (y, v `mappend` v')

?? tell函数,还没搞明白。

1
2
3
4
5
6
7
> tell :: Writer w m => w -> m ()
> tell ["mother fucker"] >> logNum 3
> WriterT (Identity (3,["mother fucker","Got: 3"]))
> logNum 3 >> tell ["mother fucker"]
> WriterT (Identity ((),["Got: 3","mother fucker"]))
> :t tell ["mother fucker"]
> tell ["mother fucker"] :: MonadWriter [[Char]] m => m ()

接下来,变魔术:

1
2
3
4
5
6
multWithLog :: Writer [String] Int
multWithLog = do
x <- logNum 3
y <- logNum 5
tell ["Gonna mul them"]
return $ x*y
1
2
> runWriter multWithLog
(15,["Got number: 3","Got number: 5","Gonna mul them"])

上面表达式用>>=显式来写:

1
multWithLog = logNum 3 >>= \x -> logNum 5 >>= \y -> tell ["Gonna mul them"] >> return $ x*y

可以看到,你好像没有做任何组装log的动作,表达式里也没有任何痕迹,但最后log就带出来了。关键在Writer>>=的定义。

给程序打log

有了Writer就可以给你的函数打log了,比如:

1
2
3
4
5
6
7
8
9
10
import Control.Monad.Writer
gcd' :: Int -> Int -> Writer [String] Int
gcd' a b
| b == 0 = do
tell ["Finish with " ++ show a]
return a
| otherwise = do
let moded = (a `mod` b)
tell [show a ++ " mod " ++ show b ++ " = " ++ show moded]
gcd' b moded

运行:

1
2
3
4
5
6
7
> fst $ runWriter $ gcd' 8 3
> 1
> mapM_ putStrLn . snd . runWriter $ gcd' 8 3
> 8 mod 3 = 2
> 3 mod 2 = 1
> 2 mod 1 = 0
> Finish with 1

只要把返回类型包一层Writer, 程序内换成do代码块或者>>=就行了。

函数作为Monad

老套路,慢慢推

1
2
3
4
5
6
7
8
9
10
(->) r a -> (a -> (->) r b) -> (->) r b
(r -> a) -> (a -> r -> b) -> (r -> b)
w = r
h = r -> a
f = a -> r -> b
h >>= f = (r -> b)
= \w -> b
= \w -> (a -> r -> b) a r
= \w -> f a w
= \w -> f (h w) w

另一个return比较简单,就不推了。

1
2
3
instance Monad ((->) r) where
return x = \_ -> x
h >>= f = \w -> f (h w) w

带状态的计算过程