一.从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 b
pure :: (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)
Just 2
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 operator
P.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
函数集合及定义在其上的>=>
运算构成幺半群,幺元是return
P.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管道一样