一.从Functor到Monad
从类型来看,Functor到Applicative再到Monad是从一般到特殊的递进过程(Monad是特殊的Applicative,Applicative是特殊的Functor)

Functor
能够把普通函数map over到一个具有context的值

fmap :: (Functor f) => (a -> b) -> f a -> f b

用来解决context相关计算中最简单的场景:怎样把一个不具context的函数应用到具有context的值?

(+1) ->? Just 1

fmap登场:

> fmap (+1) (Just 1)Just 2

Applicative

在Functor之上的增强,能够把context里的函数map over到一个具有context的值

(<*>) :: (Applicative f) => f (a -> b) -> f a -> f bpure :: (Applicative f) => a -> f a

Applicative可以理解为计算语境(computation context),Applicative值就是计算,比如:

Maybe a代表可能会失败的computation,[a]代表同时有好多结果的computation(non-deterministic computation),而IO a代表会有side-effects的computation。

P.S.关于computation context的详细信息,见Functor与Applicative_Haskell笔记7

用来解决context相关计算中的另一个场景:怎样把一个具有context的函数应用到具有context的值?

Just (+1) ->? Just 1

<*>登场:

> Just (+1) <*> (Just 1)Just2

Monad
在Applicative之上的增强,能够把一个输入普通值输出具有context值的函数,应用到一个具有context的值

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

如果你有一个具有context的值m a,你能如何把他丢进一个只接受普通值a的函数中,并回传一个具有context的值?也就是说,你如何套用一个型态为a -> m b的函数至m a?

用来解决context相关计算中的最后一个场景:怎样把一个输入普通值输出具有context的值的函数,应用到具有context的值?

\x -> Just (x + 1) ->? Just 1

=登场:

Just 1 >>= \x -> Just (x + 1)
Just 2

**三者的关联**从接口行为来看,这三个东西都是围绕具有context的值和函数在搞事情(即,context相关的计算)。那么,考虑一下,共有几种组合情况?函数输入输出类型一致的情况* context里的函数 + context里的值:Applicative* context里的函数 + 普通值:用pure包一下再调* 普通函数 + context里的值:Functor* 普通函数 + 普通值:函数调用函数输入输出类型不一致的情况* 函数输入普通值,输出context里的值 + context里的值:Monad* 函数输入普通值,输出context里的值 + 普通值:直接调用* 函数输入context里的值,输出普通值 + context里的值:直接调用* 函数输入context里的值,输出普通值 + 普通值:用pure包一下再调所以,就这个场景(把是否处于context里的函数应用到是否处于context里的值)而言,拥有Functor、Applicative和Monad已经足够应付所有情况了**二.Monad typeclass**

class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= _ -> k
return :: a -> m a
return = pure
fail :: String -> m a
fail s = errorWithoutStackTrace s

实际上,Monad实例只要求实现>>=函数(称之为bind)即可。换言之,Monad就是支持>>=操作的Applicative functor而已return是pure的别名,所以仍然是接受一个普通值并把它放进一个最小的context中(把普通值包进一个Monad里面)(>>) :: m a -> m b -> m b定义了默认实现,把函数\_ -> m b通过>>=应用到m a上,用于(链式操作中)忽略前面的计算结果P.S.链式操作中,把遇到的>>换成>>= \_ ->就很容易弄明白了P.S.上面类型声明中的forall是指∀(离散数学中的量词,全称量词∀表示“任意”,存在量词∃表示“存在”)。所以forall a b. m a -> (a -> m b) -> m b是说,对于任意的类型变量a和b,>>=函数的类型是m a -> (a -> m b) -> m b。可以省略掉forall a b.,因为默认所有的小写字母类型参数都是任意的:

In Haskell, any introduction of a lowercase type parameter implicitly begins with a forall keyword

**三.Maybe Monad**Maybe的Monad实现相当符合直觉:

instance Monad Maybe where
(Just x) >>= k = k x
Nothing >>= = Nothing
fail
= Nothing

>>=把函数k应用到Just里的值上,并返回结果,Nothing的话,就直接返回Nothing。例如:

Just 3 >>= \x -> return (x + 1)
Just 4
Nothing >>= \x -> return (x + 1)
Nothing

P.S.注意我们提供的函数\x -> return (x + 1),return的价值体现出来了,要求函数类型是a -> m b,所以把结果用return包起来很方便,并且语义也很恰当这种特性很适合处理一连串可能出错的操作的场景,比如JS的:

const err = error => NaN;
new Promise((resolve, reject) => {
resolve(1)
})
.then(v => v + 1, err)
.then(v => {throw v}, err)
.then(v => v * 2, err)
.then(console.log.bind(this), err)

一连串的操作,中间步骤可能出错(throw v),出错后得到表示错误的结果(上例中是NaN),没出错的话就能得到正确的结果用Maybe的Monad特性来描述:

return 1 >>= \x -> return (x + 1) >>= _ -> (fail "NaN" :: Maybe a) >>= \x -> return (x * 2)
Nothing

1:1完美还原,利用Maybe Monad从容应对一连串可能出错的操作**四.do表示法**在I/O场景用到过do语句块(称之为do-notation),可以把一串I/O Action组合起来,例如:

do line <- getLine; char <- getChar; return (line ++ [char])
hoho
!"hoho!"

把3个I/O Action串起来,并返回了最后一个I/O Action。实际上,do表示法不仅能用于I/O场景,还适用于任何Monad就语法而言,do表示法要求每一行都必须是一个monadic value,为什么呢?因为do表示法只是>>=的语法糖,例如:

foo = do
x <- Just 3
y <- Just "!"
Just (show x ++ y)

类比不涉及context的普通计算:

let x = 3; y = "!" in show x ++ y

不难发现do表示法的清爽简洁优势,实际上是:

foo' = Just 3 >>= (\x ->
Just "!" >>= (\y ->
Just (show x ++ y)))

如果没有do表示法,就要手动写一堆lambda嵌套:

Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y)))

所以<-的作用是:

像是使用>>=来将monadic value带给lambda一样

>>=有了,那>>呢,怎么用?

maybeNothing :: Maybe Int
maybeNothing = do
start <- return 0
first <- return ((+1) start)
Nothing
second <- return ((+2) first)
return ((+3) second)

当我们在do表示法写了一行运算,但没有用到<-来绑定值的话,其实实际上就是用了>>,他会忽略掉计算的结果。我们只是要让他们有序,而不是要他们的结果,而且他比写成_ <- Nothing要来得漂亮的多。最后,还有fail,do表示法中发生错误时会自动调用fail函数:

fail :: String -> m a
fail s = errorWithoutStackTrace s

默认会报错,让程序挂掉,具体Monad实例有自己的实现,比如Maybe:

fail _ = Nothing

忽略错误消息,并返回Nothing。试玩一下:

do (x:xs) <- Just ""; y <- Just "abc"; return y;
Nothing

在do语句块中模式匹配失败,直接返回fail,意义在于:这样模式匹配的失败只会限制在我们monad的context中,而不是整个程序的失败**五.List Monad**

instance Monad [] where
xs >>= f = [y | x <- xs, y <- f x]
(>>) = (*>)
fail _ = []

List的context指的是一个不确定的环境(non-determinism),即存在多个结果,比如[1, 2]有两个结果(1,2),[1, 2] >>= \x -> [x..x + 2]就有6个结果(1,2,3,2,3,4)P.S.怎么理解“多个结果”?初学C语言时有个困惑,函数能不能有多个return?那要怎么返回多个值?可以返回一个数组(或者结构体、链表等都行),把多个值组织到一起(放进一个数据结构),打包返回如果一个函数返回个数组,就不确定他返回了多少个结果,这就是所谓的不确定的环境从List的Monad实现来看,>>=是个映射操作,没什么好说的>>看起来有点意思,等价于定义在Applicative上的*>:

class Functor f => Applicative f where
(>) :: f a -> f b -> f b
a1
> a2 = (id <$ a1) <*> a2

class Functor f where
(<$) :: a -> f b -> f a
(<$) = fmap . const

const :: a -> b -> a
const x _ = x

作用是丢弃第一个参数中的值,仅保留结构含义(List长度信息),例如:> [1, 2] >> [3, 4, 5][3,4,5,3,4,5]等价于:

((fmap . const) id $ [1, 2]) <> [3, 4, 5]
[3,4,5,3,4,5]
-- 或者
[id, id] <
> [3, 4, 5]
[3,4,5,3,4,5]
List Comprehension与do表示法
一个有趣的示例:

[1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch)
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]

最后的n看着不太科学(看infixl 1 >>=好像访问不到),实际上能访问到n,是因为lambda表达式的贪婪匹配特性,相当于:

[1,2] >>= \n -> (['a','b'] >>= \ch -> return (n,ch))
-- 加括号完整版
([1, 2] >>= (\n -> (['a','b'] >>= (\ch -> return (n,ch)))))

函数体没界限就匹配到最右端,相关讨论见Haskell Precedence: Lambda and operatorP.S.另外,如果不确定表达式的结合方式(不知道怎么加括号)的话,有神奇的方法,见How to automatically parenthesize arbitrary haskell expressions?用do表示法重写:

listOfTuples = do
n <- [1,2]
ch <- ['a','b']
return (n,ch)

形式上与List Comprehension很像:

[ (n,ch) | n <- [1,2], ch <- ['a','b'] ]

实际上,List Comprehension和do表示法都只是语法糖,最后都会转换成>>=进行计算**六.Monad laws**同样,Monad也需要遵循一些规则:左单位元(Left identity):return a >>= f ≡ f a右单位元(Right identity):m >>= return ≡ m结合律(Associativity):(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)单位元的性质看起来不很明显,可以借助Kleisli composition转换成更标准的形式:

-- | Left-to-right Kleisli composition of monads.
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g = \x -> f x >>= g

(摘自Control.Monad)从类型声明来看,>=>相当于Monad函数之间的组合运算(monadic function),这些函数输入普通值,输出monadic值。类比普通函数组合:

(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)

>=>从左向右组合Moand m => a -> m b的函数,.从右向左组合a -> b的函数P.S.那么,有没有从右向左的Monad函数组合呢?没错,就是<=<用Kleisli composition(>=>)来描述Monad laws:左单位元:return >=> f ≡ f右单位元:f >=> return ≡ f结合律:(f >=> g) >=> h ≡ f >=> (g >=> h)满足这3条,所以是标准的Monoid,Moand m => a -> m b函数集合及定义在其上的>=>运算构成幺半群,幺元是returnP.S.用>=>描述的Monad laws,更大的意义在于这3条是形成数学范畴所必须的规律,从此具有范畴的数学意义,具体见Category theory**MonadPlus**同时满足Monad和Monoid的东西有专用的名字,叫MonadPlus:

class (Alternative m, Monad m) => MonadPlus m where
mzero :: m a
mzero = empty
mplus :: m a -> m a -> m a
mplus = (<|>)

在List的场景,mzero就是[],mplus是++:

instance Alternative [] where
empty = []
(<|>) = (++)

这有什么用呢?比如要对列表元素进行过滤的话,List Comprehension最简单:

[ x | x <- [1..50], '7' elem show x ]
[7,17,27,37,47]

用>>=也能搞定:

[1..50] >>= \x -> if ('7' elem show x) then [x] else []
[7,17,27,37,47]

条件表达式看起来有些臃肿,有了MonadPlus就可以换成更简洁有力的表达方式:

[1..50] >>= \x -> guard ('7' elem show x) >> return x
[7,17,27,37,47]

其中guard函数如下:

guard :: (Alternative f) => Bool -> f ()
guard True = pure ()
guard False = empty

输入布尔值,输出具有context的值(True对应放在缺省context里的(),False对应mzero)经guard处理后,再利用>>把非幺元值恢复成原值(return x),而幺元经过>>运算后还是幺元([]),就被滤掉了对应的do表示法如下:

sevensOnly = do
x <- [1..50]
guard ('7' elem show x)
return x

对比List Comprehension形式:

[ x | x <- [1..50], '7' elem show x ]

非常相像,都是几乎没有多余标点的简练表达**在do表示法中的作用**把Monad laws换成do表示法描述的话,就能得到另一组等价转换规则:

-- Left identity
do { x′ <- return x;
f x′
}

do { f x }

-- Right identity
do { x <- m;
return x
}

do { m }

-- Associativity
do { y <- do { x <- m;
f x
}
g y
}

do { x <- m;
do { y <- f x;
g y
}
}

do { x <- m;
y <- f x;
g y
}

这些规则有2个作用:能够用来简化代码

skip_and_get = do
unused <- getLine
line <- getLine
return line

-- 利用Right identity,去掉多余的return
skip_and_get = do
unused <- getLine
getLine
能够避免do block嵌套

main = do
answer <- skip_and_get
putStrLn answer

-- 展开
main = do
answer <- do
unused <- getLine
getLine
putStrLn answer

-- 用结合律解开do block嵌套
main = do
unused <- getLine
answer <- getLine
putStrLn answer

**七.Monad与Applicative**回到最初的场景,我们已经知道了Monad在语法上能够简化context相关计算,能够把a -> m b应用到m a上既然Monad建立在Applicative的基础之上,那么,与Applicative相比,Monad的核心优势在哪里,凭什么存在?因为applicative functor并不允许applicative value之间有弹性的交互这,怎么理解?再看一个Maybe Applicative的示例:

Just (+1) <> (Just (+2) <> (Just (+3) <*> Just 0))
Just 6

中间环节都不出错的Applicative运算,能够正常得到结果。如果中间环节出错了呢?

-- 中间失败

Just (+1) <> (Nothing <> (Just (+3) <*> Just 0))
Nothing

也符合预期,纯Applicative运算似乎已经满足需要了。仔细看看刚才是如何表达中间环节的失败的:Nothing <*> some thing。这个Nothing就像是硬编码装上去的炸弹,是个纯静态场景那想要动态爆炸的话,怎么办?

-- 灵活性不足

Just (+1) <> (Just (\x -> if (x > 1) then Nothing else return (x + 2)) <> (Just (+3) <*> Just 0))
<interactive>:85:1: error:
• Non type-variable argument in the constraint: Num (Maybe a)
(Use FlexibleContexts to permit this)
• When checking the inferred type
it :: forall a. (Ord a, Num (Maybe a), Num a) => Maybe (Maybe a)

出错的原因是试图动态控制爆炸,却搞出来一个Maybe (Maybe a):

> Just (\x -> if (x > 1) then Nothing else return (x + 2)) <*> (Just (+3) <*> Just 0)Just Nothing

之所以会出现这样尴尬的局面,是因为Applicative的<*>只是机械地从左侧context里取出函数,应用到右侧context里的值上。从Maybe取函数只有两种结果:要么从Nothing取不出东西来,立即爆炸;要么从Just f取出个f,运算得到Just (f x),上一步(x)没炸的话就炸不了了

所以,从应用场景来看,Monad是一种计算语境控制,应对一些通用场景,比如错误处理,I/O,不确定结果数量的计算等等,其存在意义是:比Applicative更灵活,允许在每一步计算中添加控制,像Linux管道一样

参考资料
Monad

The forall keyword

Monad laws

Explanation of Monad laws

更多相关文章

  1. 从真实场景聊聊为啥Alfred能提高效率
  2. 面试必问:布隆过滤器的原理以及使用场景
  3. 函数式编程中如何处理副作用?
  4. 用户画像分析与场景应用
  5. 设计模式使用场景、优缺点汇总
  6. 帆软报表自定义函数-取json数据
  7. 函数和递归
  8. java的getClass()函数
  9. 函数的学习

随机推荐

  1. 驾考一点通 android
  2. 扣丁学堂笔记第05天高级UI组件(一)
  3. Android之AudioRecord实现"助听器"
  4. Androd学习笔记——Conflict between And
  5. Android中的Drawable资源—— ScaleDrawa
  6. Qt平台下OpenCV for Android库的顺序
  7. Android中定义数组与使用
  8. Android RelativeLayout属性介绍
  9. Android将多个视频文件拼接为一个文件
  10. android底层的学习