原文地址:Haskell学习-monad
什么是Monad
Haskell是一门纯函数式的语言,纯函数的优点是安全可靠。函数输出完全取决于输入,不存在任何隐式依赖,它的存在如同数学公式般完美无缺。可是纯函数因为隔绝了外部环境,连最基本的输入输出都无法完成。而 Monad
就是 Haskell 给出的解决方案。但Monad
并不仅仅是 IO 操作的抽象,它更是多种类似操作之间共性的抽象。所以 Monad 解决的问题并不局限在 IO 上,像 Haskell 中的 Maybe
和 []
都是 Monad
。Haskell 中漂亮的错误处理方式, do
表示法和灵活的列表推导式 (list comprehension
) 都算是 Monad
的贡献。
Monad
基本上是一种加强版的 Applicative Functor
,正如 Applicative Functor
是 Functor
的加强版一样。所以在充分理解 Applicative Functor
的基础上,过渡到 Monad
其实是非常平滑的。
-- Monad的定义
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y
fail :: String -> m a
fail msg = error msg
-
return
跟其他语言中的return
是完全不一样的,它是一个把普通值包进一个 context 里面的函数,并不是结束函数执行的关键字。其实等价于Applicative中的pure
。 -
>>
忽略前面表达式的返回值,直接执行当前表达式。 -
>>=
接受一个 monadic value(也就是具有 context 的值,可以用装有普通值的盒子来比喻)并且把它喂给一个接受普通值的函数,并回传一个 monadic value。 -
=<<
和上面>>=
功能一样,只是结合顺序相反。
Monad 的原理
函数之间要协作,就必须以各种形式交互连接。但如何隔离纯函数与副作用函数,同时又能让两类函数相互复用呢?
以 IO 操作为例子分析,为了充分隔离纯函数与 IO 函数,Haskell 中不能实现 IO Char -> Char
这样一种输入是 IO 类型返回值却是普通类型的函数。否则副作用函数就能很容易变身为纯函数了。事实上一旦参数中有 IO,返回值必有 IO,这就保证了充分隔离。
那如何让纯函数与 IO 函数相互复用呢?这就要靠 IO Monad 中定义的 return
和 >>=
这两个函数了。return
(在 Haskell 中不是关键字,只是一个函数名)的作用是将某个类型为 A
的值 a
提升(装箱)为类型为 IO A
的值 Char -> IO Char
。有了这个函数后,纯函数就可以通过 return
变成返回值为 IO 带副作用的函数了。
有了提升而没有下降操作,怎么复合 putChar :: Char -> IO()
与 getChar :: IO Char
呢。 getChar 从 IO 读取一个字符, putChar 把字符写入 IO。但 getChar
返回的是 IO Char
类型,而 putChar
需要的是普通的 Char
类型,两者不匹配怎么办? >>=
函数出马了! >>=
的类型是
IO a -> (a -> IO b) -> IO b
这样 >>=
就可以连接 getChar
与 putChar
,把输入写到输出中
getChar >>= putChar
可以看到 >>=
操作实际上是类型下降(或拆箱)操作,同时执行下降操作的函数返回值也必须是 IO 类型。这样既充分隔离纯函数与副作用函数,又能让函数相互复用。通过 return
和 >>=
两个平行世界 (范畴) 就有了可控的交流通道。
do 表示法
Haskell的 do 表示法实际上是Monad的语法糖:它给我们提供了一种不使用 >>=
和匿名函数来写monadic代码的方式。去除do语法糖的过程就是把它转换为 >>=
和匿名函数。
do 表示法可以使用分号 ;
和大括号 { }
将语句分块;但一般会使用一个表达式一行的方式,不同的作用域用不同的缩进区分。
我们还是以IO 为例子,接受两次的键盘输入,然后将两次输入的字符串合并成一个字符串,最后屏幕打印输出。 >>=
会接受前面表达式的值;>>
则会忽略前面表达式的值;这里使用 return
实际它返回的仍然是IO String,因为Haskell会自动类型推导得出。monadic 的表达式代码如下:
(++) <$> getLine <*> getLine >>= print >> return "over"
111
222
> "111222"
> "over"
使用 do改写,明显更加清晰,和我们熟悉的命令式语言风格差不多。
<-
表示从monadic value中取出普通值,可以看成是拆开盒子取出所需要的值。
foo :: IO String
foo = do
x <- getLine
y <- getLine
print (x ++ y)
return "over"
do语法对应模式
do {e} -> e
do {e; es} -> e >> do {es}
do {let decls; es} -> let decls in do {es}
do {p <- e; es} -> e >>= \p -> es
Monad 类型
来看一下几个默认的Monad类型,它们都必须实现 return
,>>=
,fail
这几个函数。
-
Maybe
中间任何一步只要有Nothing
,结果就提前返回Nothing
。没有任何意外的情况才返回Just 值
。-- Maybe 的 Monad instance instance Monad Maybe where return x = Just x Nothing >>= f = Nothing Just x >>= f = f x fail _ = Nothing -- 实例 Just 3 >>= (\x -> Nothing >>= (\y -> Just (show x ++ y))) > Nothing Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y))) > Just "3!"
使用 do 表示法写成这样:
foo :: Maybe String foo = do x <- Just 3 y <- Just "!" Just (show x ++ y)
-
List
>>=
基本上就是接受一个有 context 的值,把他喂进一个只接受普通值的函数,并回传一个具有 context 的值。[]
其实等价于 Nothing。当我们用
>>=
把一个 list 喂给这个函数,lambda 会映射每个元素,会计算出一串包含一堆 list 的 list,最后再把这些 list 压扁,得到一层的 list。这就是我们得到 列表list
处理 Mondic value 的过程。--list 的 Monad instance instance Monad [] where return x = [x] xs >>= f = concat (map f xs) fail _ = [] -- 实例 [3,4,5] >>= \x -> [x,-x] > [3,-3,4,-4,5,-5] [1,2,3] >>= \x -> return (-x) > [-1,-2,-3]
list comprehension
也不过是Monad
的语法糖[1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch) -- Monad [ (n,ch) | n <- [1,2], ch <- ['a','b'] ] -- list comprehension > [(1,'a'),(1,'b'),(2,'a'),(2,'b')]
list comprehension
的过滤基本上跟 guard 是一致的。[1..50] >>= (\x -> guard ('7' `elem` show x) >> return x) > [7,17,27,37,47]
用
do
改写, 如果不写最后一行return x
,那整个 list 就会是包含一堆空tuple
的 list。sevensOnly :: [Int] sevensOnly = do x <- [1..50] guard ('7' `elem` show x) return x -- 对应的 list comprehension [ x | x <- [1..50], '7' `elem` show x ] > [7,17,27,37,47]
-
Either
在Control.Monad.Error
里面有Error
的Monad instance
。instance (Error e) => Monad (Either e) where return x = Right x Right x >>= f = f x Left err >>= f = Left err fail msg = Left (strMsg msg) Right 3 >>= \x -> return (x + 100) :: Either String Int > Right 103
Monad 规则
-
return a >>= f == f a
== 左边的表达式等价于右边的表达式。如果仅仅是把一个值包装到monad里面然后使用 (>>=
) 调用的话,我们就没有必要使用return
;这条规则对于我们的代码风格有着实际的指导意义:我们不应该写一些不必要的代码;这条规则保证了简短的写法和冗余的写法是等价的。return 3 >>= (\x -> Just (x+100000)) -- 和直接函数调用没有区别
-
m >>= return == m
这一条规则对风格也有好处:如果在一系列的action块里面,如果最后一句就是需要返回的正确结果,那么就不需要使用 return 了;和第一条规则一样,这条规律也能帮助我们简化代码。Just "move on up" >>= return -- 可以不需要 return
-
(m >>= f) >>= g == m >>= (\x -> f x >>= g)
当我们用 >>= 把一串 monadic function 串在一起,他们的先后顺序不应该影响结果。
而这不就是结合律吗?我们可以把那些子action提取出来组合成一个新action。
(<=<
) 可以用来合成两个 monadic functions, 类似于普通函数结合(.
), 而(>=>
) 表示结合顺序相反。(<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> (a -> m c) f <=< g = (\x -> g x >>= f) -- 普通函数结合(.) let f = (+1) . (*100) f 4 > 401 -- 合成monadic functions (<=<) let g = (\x -> return (x+1)) <=< (\x -> return (x*100)) Just 4 >>= g > Just 401 -- 也可以将 monadic 函数用foldr,id 和(.)合成 let f = foldr (.) id [(+1),(*100),(+1)] f 1 > 201
Monad 的 (->) r 形态
(->) r
不只是一个 functor
和 applicative functor
,同时也是一个 monad
。
每一个 monad
都是个 applicative functor
,而每一个 applicative functor
也都是一个 functor
。尽管 moand
有 functor
跟 applicative functor
的性质,但他们不见得有 Functor
跟 Applicative
的 instance 定义。
instance Monad ((->) r) where
return x = \_ -> x
h >>= f = \w -> f (h w) w
Monad 辅助函数
带下划线函数等价于不带下划线的函数, 只是不返回值
>>= :: m a -> (a -> m b) -> m b
=<< :: (a -> m b) -> m a -> m b
form :: t a -> (a -> m b) -> m (t b)
form_ :: t a -> (a -> m b) -> m ()
mapM :: (a -> m b) -> t a -> m (t b)
mapM_ :: (a -> m b) -> t a -> m ()
filterM :: (a -> m Bool) -> [a] -> m [a]
foldM :: (b -> a -> m b) -> b -> t a -> m b
sequence :: t (m a) -> m (t a)
sequence_ :: t (m a) -> m ()
liftM :: (a1 -> r) -> m a1 -> m r
when :: Bool -> f () -> f ()
join :: m (m a) -> m a
其中在 IO 中经常用到的一些函数
-
sequence
sequence
接受一串 I/O action,并回传一个会依序执行他们的 I/O action。运算的结果是包在一个 I/O action 的一连串 I/O action 的运算结果。main = do a <- getLine b <- getLine c <- getLine print [a,b,c]
其实可以写成
main = do rs <- sequence [getLine, getLine, getLine] print rs
一个常见的使用方式是我们将
print
或putStrLn
之类的函数 map 到串列上。sequence (map print [1,2,3,4,5]) 1 2 3 4 5 [(),(),(),(),()]
-
mapM
跟mapM_
由于对一个串列 map 一个回传 I/O action 的函数,然后再sequence
这个动作太常用了。所以函式库中提供了mapM
跟mapM_
。mapM
接受一个函数跟一个串列,将对串列用函数 map 然后 sequence 结果。mapM_
也作同样的事,只是他把运算的结果丢掉而已。在我们不关心 I/O action 结果的情况下,mapM_
是最常被使用的。mapM print [1,2,3] 1 2 3 [(),(),()] mapM_ print [1,2,3] 1 2 3
form
和form_
与mapM
和mapM_
类似,不过只是把列表参数提前。
还有一些是在 monad
中定义,且等价于 functor
或 applicative functor
中所具有的函数。
-
liftM
liftM
跟fmap
等价, 也有liftM3
,liftM4
跟liftM5
liftM :: (Monad m) => (a -> b) -> m a -> m b liftM f m = m >>= (\x -> return (f x)) liftM (*2) [1,2] > [2,4]
-
ap
ap
基本上就是<*>
,只是他限制在 Monad 上而不是 Applicative 上。ap :: (Monad m) => m (a -> b) -> m a -> m b ap mf m = do f <- mf x <- m return (f x) ap [(*2)] [1,2,3] > [2,4,6]
-
join
m >>= f
永远等价于join (fmap f m)
这性质非常有用join :: (Monad m) => m (m a) -> m a join (Just (Just 9)) > Just 9 join [[1,2,3],[4,5,6]] -- 对于 list 而言 join 不过就是 concat > [1,2,3,4,5,6]
-
filterM
filterM
,除了能做 filter 的动作,同时还能保有 monadic context。filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a] filterM (\x -> return (x > 2)) [1,2,3,4] > [3,4]
-
foldM
foldl
的 monadic 的版本叫做foldM
foldM :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m a foldM (\x y -> return (x + y)) 0 [1,2,3] > 6