单子#
简介#
- 单子:一种设计模式,将多个程序片段绑定在一起,并将结果封装到计算上下文中;
- 单子实际上是适用函子的加强版;
单子类型类#
class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
Monad
类型类:任何单子都是适用函子,且定义了上述三种方法;Monad
类型类只接受种类为* -> *
的类型构造器;
单子方法#
-
GHC.Base.
return
:: Monad m => a -> m a# 接受一个值,将值打包进单子中,返回一个单子。
该函数和
pure
函数实际上是同一函数。源码
1return :: a -> m a 2return = pure
-
GHC.Base.
(>>=)
:: Monad m => m a -> (a -> m b) -> m b# 读作绑定。接受一个单子和一个函数,该函数接受一个值并返回另一个单子,对单子应用该函数后返回该函数返回的单子。
可以理解为将第一个单子中的值取出,对该值应用函数后获得新的单子。
exp1 = Just 2 >>= (\x -> return $ x + 1) -- Just 3 exp2 = Right "Hello" >>= (\x -> return $ x ++ "!") -- Right "Hello!"
-
GHC.Base.
(>>)
:: Monad m => m a -> m b -> m b# 接受两个单子,对两个单子顺序求值,丢弃第一个单子的返回值,保留第二个单子的返回值。
exp3 = Just 4 >> return 5 -- Just 5 exp4 = getLine >> getLine -- Alice -- Amber -- "Amber"
源码
1(>>) :: forall a b. m a -> m b -> m b 2m >> k = m >>= \_ -> k
单子成员#
[]
与适用函子类似,由于列表为非确定性的,
<-
操作符会遍历列表所有元素并应用函数,最后提取所有结果中的元素为一个大列表;exp1 = [1, 2, 3] >>= \x -> return $ x + 1 -- [2,3,4] exp2 = [1, 2, 3] >> [1] -- [1,1,1]
列表推导式实际上是列表单子语法的语法糖,列表推导式和
do
表示法最终都会翻译为>>=
运算符和匿名函数;exp3 = [ (n, ch) | n <- [1, 2], ch <- ['a', 'b'] ] exp4 = do n <- [1, 2] ch <- ['a', 'b'] return (n, ch) exp5 = [1, 2] >>= \n -> ['a', 'b'] >>= \ch -> return (n, ch)
1type KnightPos = (Int, Int) 2 3-- | 在 8 * 8 大小的国际象棋棋盘上移动骑士。 4moveKnight :: KnightPos -> [KnightPos] 5moveKnight (c, r) = filter 6 (\(c, r) -> c `elem` [1 .. 8] && r `elem` [1 .. 8]) 7 [ (c + 1, r + 2) 8 , (c + 1, r - 2) 9 , (c - 1, r + 2) 10 , (c - 1, r - 2) 11 , (c + 2, r + 1) 12 , (c + 2, r - 1) 13 , (c - 2, r + 1) 14 , (c - 2, r - 1) 15 ] 16 17-- | 移动骑士 3 次,返回第 3 次所有可能的坐标。 18in3 :: KnightPos -> [KnightPos] 19in3 start = return start >>= moveKnight >>= moveKnight >>= moveKnight 20 21-- | 判断骑士从特定坐标出发并移动 3 次后是否能到达指定坐标。 22-- 23-- ==== __例子:__ 24-- >>> (6, 2) `canReachIn3` (6, 1) 25-- True 26-- 27-- >>> (6, 2) `canReachIn3` (7, 3) 28-- False 29canReachIn3 :: KnightPos -> KnightPos -> Bool 30canReachIn3 start dest = dest `elem` in3 start
源码
1instance Monad [] where 2 {-# INLINE (>>=) #-} 3 xs >>= f = [ y | x <- xs, y <- f x ] 4 {-# INLINE (>>) #-} 5 (>>) = (*>)
Maybe
若为
Nothing
,则返回Nothing
,否则对单子应用函数;exp6 = return "WHAT" :: Maybe String -- Just "WHAT" exp7 = Just 9 >>= \x -> return $ x * 10 -- Just 90
1type Birds = Int 2type Pole = (Birds, Birds) 3 4-- | 指定数量的鸟停在杆子左侧。 5-- 若左右鸟的数量差大于 3,则拿杆人失去平衡。 6landLeft :: Birds -> Pole -> Maybe Pole 7landLeft n (l, r) | abs (l + n - r) < 4 = Just (l + n, r) 8 | otherwise = Nothing 9 10-- | 指定数量的鸟停在杆子右侧。 11-- 若左右鸟的数量差大于 3,则拿杆人失去平衡。 12landRight :: Birds -> Pole -> Maybe Pole 13landRight n (l, r) | abs (r + n - l) < 4 = Just (l, r + n) 14 | otherwise = Nothing 15 16-- | 若拿杆人踩到香蕉皮,则立即失去平衡。 17banana :: Pole -> Maybe Pole 18banana _ = Nothing 19 20-- >>> routines 21-- [Nothing,Just (4,2),Nothing,Nothing] 22routines :: [Maybe Pole] 23routines = 24 [ return (0, 0) >>= landLeft 1 >>= landRight 4 >>= landLeft (-1) 25 , return (0, 0) >>= landLeft 2 >>= landRight 2 >>= landLeft 2 26 , return (0, 0) >>= landLeft 1 >>= banana >>= landRight 1 27 , return (0, 0) >>= landLeft 1 >> Nothing >>= landRight 1 28 ]
源码
1instance Monad Maybe where 2 (Just x) >>= k = k x 3 Nothing >>= _ = Nothing 4 5 (>>) = (*>)
IO
exp8 = getLine >>= readFile >>= putStrLn -- 输入文件名,并打印文件内容
Either e
exp9 = Left "Error" >>= undefined -- Left "Error"
源码
1instance Monad (Either e) where 2 Left l >>= _ = Left l 3 Right r >>= k = k r
(->) r
exp10 = (+ 1) >>= (\x -> return $ x * 2) $ 3 -- 8
源码
1instance Monad ((->) r) where 2 f >>= k = \ r -> k (f r) r
单子规则#
规则一:左同等
return a >>= k = k a
exp1 = return 3 >>= \x -> Just (x + 1) -- Just 4
exp2 = (\x -> Just (x + 1)) 3 -- Just 4
exp3 = return "H" >>= \x -> [x, x, x] -- ["H", "H", "H"]
exp4 = (\x -> [x, x, x]) "H" -- ["H", "H", "H"]
规则二:右同等
m >>= return = m
exp5 = Just 3 >>= return -- Just 3
exp6 = "Hello" >>= return -- "Hello"
规则三:结合
m >>= (\x -> k x >>= h) = (m >>= k) >>= h
exp7 = (getLine >>= readFile) >>= putStrLn
exp8 = getLine >>= (\fname -> readFile fname >>= putStrLn)
do
表示法#
do
<variable> <- <Monad>
<Monad>
...
do { <variable> <- <Monad>; <Monad>; ... }
do
表示法:do
语句块除了能用于输入输出外,还能用于所有单子,可将多个单子操作链接起来;do
表示法实际是单子语法的语法糖;将
>>=
运算符和匿名函数简化为<-
操作符;1foo :: Maybe String 2foo = Just 3 >>= (\x -> 3 Just "!" >>= (\y -> 4 Just (show x ++ y))) 5 6foo' :: Maybe String 7foo' = do 8 x <- Just 3 9 y <- Just "!" 10 Just (show x ++ y)
将
>>
运算符省略;1foo :: Maybe Int 2foo = Just 3 >> 3 Nothing >>= (\x -> 4 return $ x + 1) -- Nothing 5 6foo' :: Maybe Int 7foo' = do 8 x <- Just 3 9 Nothing -- 不使用 @<-@ 绑定变量,则效果同 @>>@ 10 -- 和 @_ <- Nothing@ 等效,但更简洁 11 return $ x + 1 -- Nothing
do
表示法中一行书写一个单子,最后一行不能使用<-
运算符,因为将do
表示法翻译回>>=
运算符和匿名函数就能发现,最后一个单子的结果代表整个do
表示法的结果;do
表示法也可以将所有单子书写在一行,单子间用分号分隔,但不推荐这种格式;foo :: Maybe Int foo = do x <- Just 3 y <- Just 4 return $ x + y -- Just 7 foo' :: Maybe Int foo' = do { x <- Just 3; y <- Just 4; return $ x + y } -- Just 7
do
表示法允许模式匹配,模式匹配失败会调用Control.Monad.Fail.fail
函数;1foo :: Maybe Char 2foo = Just "Hello" >>= \(x : _) -> return x -- Just 'H' 3 4foo' :: Maybe Char 5foo' = do 6 (x : _) <- Just "Hello" 7 return x -- Just 'H' 8 9failedFoo' :: Maybe Char 10failedFoo' = do 11 (x : _) <- Just "" 12 return x -- Nothing
源码
1{-# LANGUAGE Trustworthy #-} 2{-# LANGUAGE NoImplicitPrelude #-} 3 4module Control.Monad.Fail ( MonadFail(fail) ) where 5 6import GHC.Base (String, Monad(), Maybe(Nothing), IO(), failIO) 7 8class Monad m => MonadFail m where 9 fail :: String -> m a 10 11 12instance MonadFail Maybe where 13 fail _ = Nothing 14 15instance MonadFail [] where 16 {-# INLINE fail #-} 17 fail _ = [] 18 19instance MonadFail IO where 20 fail = failIO
do
表示法虽然看上去与命令式语言相似,但实际上只是顺序求值,前一行的求值结果会影响后一行;