单子#

简介#

  • 单子:一种设计模式,将多个程序片段绑定在一起,并将结果封装到计算上下文中;
  • 单子实际上是适用函子的加强版;

单子类型类#

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表示法虽然看上去与命令式语言相似,但实际上只是顺序求值,前一行的求值结果会影响后一行