《Haskell趣学指南》笔记之 Monad

monad 是增强版的 applicative 函子。bash

我如今有点忘了什么是 applicative 函子,因此我先复习一下。app

复习

函子(Functor 类型类)

支持 fmap 的类型就是函子,好比 Maybe fmap 的定义是 (a -> b) -> f a -> f b 其意义是把一个容器里的值,通过一个函数加工一下,而后放回同样的容器里。dom

Applicative 类型类

支持 pure 和 <*> 的类型就是 applicative 的实例,好比 Maybe pure 的定义是 a -> f a,其做用是把一个值装进容器里 <*> 的定义是 f (a -> b) -> f a -> f b 其意义跟 fmap 很类似,区别在于那个函数也在容器里。函数

可是有些时候容器比喻并不那么有用,好比「(->) r 也是 Application 的实例」那里。spa

换个角度再说一遍

假设一,咱们有设计

  • 类型为 A 的普通值
  • 普通函数 A -> B
  • 想获得类型为 B 的返回值

那么用直接调用普通函数便可。code

假设二,咱们有cdn

  • 类型为 Maybe A 的奇特值
  • 普通函数 A -> B
  • 想获得类型为 Maybe B 的返回值

那么咱们就要求 Maybe 知足 fmap 的条件,而后调用 fmap 和普通函数blog

ghci> fmap (++ "!") (Just "wisdom")
Just "wisdom!"
ghci> fmap (++ "!") Nothing
Nothing
复制代码

假设三,咱们有ci

  • 类型为 Maybe A 的奇特值
  • 奇特函数 Maybe (A -> B)
  • 想获得类型为 Maybe B 的返回值

那么咱们就要求 Maybe 知足 pure 和 <*>,而后调用 <*> 和奇特函数

ghci> Just (+3) *> Just 3 
Just 6
ghci> Nothing <*> Just "greed"
Nothing
ghci> Just ord <*> Nothing
Nothing
-- pure 的意义是让普通函数也能用
-- max <$> 等价于 pure max <*>
ghci> max <$> Just 3 <*> Just6
Just 6
ghci> max <$> Just 3 <*> Nothing
Nothing
复制代码

假设四,咱们有

  • 类型为 Maybe A 的奇特值
  • 函数 A -> Maybe B(若是函数是 A -> B,那么很容易变成 A -> Maybe B,由于 B -> Maybe B 很容易实现)
  • 想获得类型为 Maybe B 的返回值

这就是 Monad 要解决的问题。

怎么作到这一点呢?咱们仍是以 Maybe 为例子(由于 Maybe 实际上确实是 monad)。

先把问题简化一下:

  • 类型为 A 的普通值
  • 函数 A -> Maybe B
  • 想获得类型为 Maybe B 的返回值

这就很简单了,直接调用函数便可。

接下来考虑若是把参数从「类型为 A 的普通值」改成「类型为 Maybe A 的奇特值」,咱们须要额外作什么事情。

很显然,若是参数是 Just A,就取出 A 值调用函数;若是参数是 Nothing,就返回 Nothing。

因此实现以下:

applyMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b 
applyMaybe Nothing f = Nothing
applyMaybe (Just x) f = f x
复制代码

Haskell 把 applyMaybe 叫作 >>=

Monad 类型类的定义

class Monad m where
    return :: a -> m a  -- 跟 pure 同样
    
    (>>=) :: m a -> (a -> m b) -> m b
    
    -- 书上说先不用管下面的 >> 和 fail
    (>>)  :: m a -> m b -> m b
    x >> y =   x >>= \_ -> y 
    
    fail :: String -> m a
    fail msg = error msg
    
复制代码

理论上一个类型成为 Monad 的实例以前,应该先成为 Applicative 的实例,就如同 class (Functor f) => Applicative f where 同样。 可是这里的 class Monad 并无出现 (Applicative m) 是为何呢?

书上说是由于「在Haskell设计之初,人们没有考虑到applicative函子会这么有用」。好吧,我信了。

Maybe 是 Monad 的实例

instance Monad Maybe where
    return x = Just x
    Nothing >>= f = Nothing
    Just x >>= f = f x
    fail _ = Nothing
复制代码

用一下

ghci> return "WHAT" :: Maybe String
Just "WHAT"
ghci> Just 9 >>= \x -> return (x*10) -- 注意 return 不是退出
Just 90
ghci> Nothing >>= \x -> return (x*10)
Nothing
复制代码

Monad 有什么意义?

上文说道

假设四,咱们有

  • 类型为 Maybe A 的奇特值
  • 函数 A -> Maybe B
  • 想获得类型为 Maybe B 的返回值

这就是 Monad 要解决的问题。

那咱们为何要研究这个问题?

书上举了一个例子,我这里简述一下。

参数 Maybe A 有两个可能,一是 Just A,而是 Nothing。

咱们把 Just A 当作是成功的 A,把 Nothing 当作是失败。

那么函数 A -> Maybe B 就是可以对成功的 A 进行处理的一个函数,它的返回值是成功的 B 或者失败。

若是还有一个函数 B -> Maybe C,就能够继续处理成功的 B 了。

这很像 Promise !

  • 参数为 Promise<User>
  • 函数为 User -> Promise<Role>(大部分时候咱们的函数是 User -> Role,可是要把 Role 变成 Promise<Role> 很简单,Promise.resolve 就能作到)
  • 而后咱们就能够把上面两个东西连起来,获得 Promsie<Role> 了!

可是注意我没有说 Promise 就是 Monad,我目前还不知道究竟是不是。

对应的 Haskell 代码就像这样

return A >>= AToMaybeB >>= BToMaybeC >>= CToMaybeD 
复制代码

对应的 JS 代码

PromiseUser.then(UserToRole).then(RoleToElse)
复制代码

do 语法

普通函数里有

let x=3; y="!" in show x ++ y
复制代码

若是把 x 和 y 都放到 Maybe 里,而后用 >>= 连起来,是这样

Just 3 >>= (\x -> 
    Just "!" >>= (\y -> 
        Just (show x ++ y )))
复制代码

为了免去这种麻烦的写法,咱们能够用 do

foo :: Maybe String
foo = do
    x <- Just 3
    y <- Just "!"
    Just (show x++y )
复制代码

因此 do 只是把 monad 值串起来的语法罢了。(没错 IO 也是 monad)

do 要求里面每一行 <- 的右边都是一个 monad 值。上例中每一行都是一个 Maybe 值。

模式匹配与 fail

justH :: Maybe Char
justH = do
    (x:xs) <- Just "hello"
    return x
复制代码

最终 justH 的值是 Just 'h'。 可是若是模式匹配失败了会怎么办?

wopwop :: Maybe Char
wopwop = do
    (x:xs) <- Just ""
    return x
复制代码

"" 是一个空的列表,没有办法获得 x,那么就会调用 monal 的 fail,fail 的默认实现是

fail :: (Monad m) => String -> m a
fail msg = error msg
-- 可是 Maybe 的 fail 是这样实现的
fail _ = Nothing
复制代码

因此若是匹配失败,会获得 Nothing,以免程序崩溃,这很巧妙。

列表是 Monad

instance Monad [] where
    return x         = [x]
    xs >>= f        = concat (map f xs)
    fail _            = []
复制代码

使用示例:

ghci> [3,4,5] >>= \x -> [x,x]
[3,-3,4,-4,5,-5]
ghci> [] >>= \x -> ["bad","mad","rad"]
[]
ghci> [1,2,3] >>= \x -> []
[]
ghci> [1,2] >>= \n -> ['a','b'] >>= \ch -> return(n,ch)
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]
-- 用 do 改写
listOfTuples :: [(Int,Char)]
listOfTuples = do
    n<-[1,2]
    ch<-['a','b']
    return (n,ch)
复制代码

monad 定律

Haskell 没法检查一个类型是否知足 monad 定律,须要开发者本身确保。

  1. 左单位元律——return x >>= f 的值必须和 f x 同样
  2. 右单位元律——m >>= returnm 的值必须同样
  3. 结合律——(m >>= f) >>= g 和 m >>= (\x -> f x >>= g) 同样
相关文章
相关标签/搜索