使用 Monad 的>>=实现<$>和<*>
惊为天人,惊为天人啊,没想到仅使用 Monad 的>>=
和return
便可以实现<$>
和<*>
!下面描述一下我的心路历程。
在一切发生之前,先来梳理一下我们手头的工具。我们只能使用 Monad 中直接定义的方法,即——
1 |
|
就这些!开始表演魔法吧。
<$>
首先是<$>
即fmap
,先看其签名——
1 |
|
在这里,函数a -> b
和变量m a
就是我们的已知量,我们要用这两个素材得到m b
。容易意识到,我们只需要对函数a -> b
进行变换,根据这个函数构造一个函数a -> m b
即可,这样得到的函数签名刚好就是flip (>>=)
。
而如何进行这种构造?整个 lambda,在 lambda 上下文中调用函数a -> b
并返回一个m b
即可!
1 |
|
这个形式可以化简,但为了明确,这里不化简。
有了这个 magic,我们就能够构造 fmapM 了!
1 |
|
<*>
<*>
的签名如下——
1 |
|
这里,最关键的问题是,该如何把这个a -> b
从 Monad 的上下文里拿出来(否则无法进行使用!)。
显然,a -> b
(以及任何其它变量)是不能直接赤果果直接放到“全局作用域”的——它们在上下文中,只能通过模式匹配或 getter 拿出来,甚至对于 IO,这些手段也是无效的。
但是它又确实是能拿出来的……之前一切的学习都没有否定这一点…这就意味着,把它拿出来必须要有一定条件,也就是说,要在一定上下文中!显然,离开具体的数据类型,我们能构造的唯一的上下文就是 lambda 了!
于是我们又开始重新查看手头的工具,好像(>>=)
给我们提供了这样一个上下文!再次查看(>>=)
的签名——
1 |
|
我们看到了什么?m a
中的值a
在(a -> m b)
函数中被进行应用了!
(>>=)
能提供我们这样的上下文!把值取出来,进行特定操作,再放回上下文中!
比如,我们有一个Just 1
,我们想给它加 1,于是我们可以这样用——
1 |
|
然后我们查看 fmap 的签名,好像 fmap 也能干这事!
1 |
|
于是破案了,纠结这么久,真实原因是我太傻,对 lambda 的意义了解不够深刻,遇上复杂问题就抓瞎了!
带着我们的新思路,再次查看我们所要实现的东西——
1 |
|
显然,通过上面的工具,我们能在 lambda 里把m (a -> b)
中的a -> b
解出来,比如这样——
1 |
|
这里的 f 就是a -> b
!在这个 lambda 的上下文里,我们有a -> b
,有m a
,显然这里该用fmap
了!
1 |
|
BINGO!
但是稍微再瞅瞅呢?我们把 fmap 重新展开成使用>>=
和return
构造的形式(以及删掉一些括号)——
1 |
|
这为什么合法?加点括号——
1 |
|
(ma >>= \a -> (return $ f a))
先执行,返回一个m b
,结果直接作为(\f -> _)
的返回值。真正的计算已经结束(如果有Nothing
之类的,那就根本没有计算),得到的值会一直往外传递,最终作为allMap
函数的返回值。
显然,这里存在着某种模式——我们可以同时将多个 Monad 中的值取出来并进行操作,最后再封装成新的 Monad。
使用这个思想,我们重新实现一下 fmap——
1 |
|
清晰!
一些更酷的东西
更酷(或许没有再酷了!)的是,do 语法糖是可以翻译成>>
和>>=
的!见下面的代码——
1 |
|
当我们要副作用的时候,我们使用>>
,当我们要从一个 Monad 里取出值的时候,我们使用>>=
,do 就是这样!
另一个很酷的地方是,列表推导式也是可以通过 do 进行描述的!比如下面的两个使用是等价的——
1 |
|
令人感叹,令人感叹啊。再次感叹 Haskell 设计的精巧。顺便,据群里大佬所说,使用 Monad 实现<*>
是”SKI 那个 S”,只能说虽不明但觉厉。该沉下心来继续学习了!
关于>>
>>
的行为其实没必要说那么多,它的定义就足够了——
1
2
3
4
5
6
7
8
9
10
(>>) :: Monad m => m a -> m b -> m b
m >> k = m >>= (\_ -> k)
-- 翻译成 do,就是——
do
ma
mb
-- 或者可以这样理解——
do
_ <- ma
mb因此,当 m 本身为
[]
或Nothing
等的时候,这计算本身就不会再进行下去了。要理解>>
,直接从定义出发就好。于是归根结底,还是要回到>>=
,还是要回到 bind,还是要回到flatMap。同时,从这个方面来看,下面的guard函数就十分容易理解了,考虑护卫语句的定义,它可能返回一个
m ()
,也可能返回一个mzero
。在列表的上下文里,即[()]
或[]
。当为前者的时候,它有一个值,因此计算成功继续;为后者的时候,没有值就无法进行了。
1
2
3
4
do
i <- [1, 2, 3]
guard $ i % 2 == 0
i在这里,
guard $ i % 2 == 0
要么返回[()]
,要么返回[]
,因此整个代码可以理解为
1
2
3
4
do
i <- [1, 2, 3]
_ <- if i `mod` 2 == 0 then [()] else [] -- 当然,_在这里是不合法的!
return i结果是显然的。
—— 2022.04.04
我曾以为>>
是干脆地丢弃前者的结果,返回后者,但事实证明这是错误的——前者的值对后者将会有影响——下面两个例子可见一般——
1 |
|
显然,当前者为 mempty(实际上是 mzero)的时候,值为后者,否则为 mempty。
因此,我们可以试图定义自己的 guard——
1 |
|
至于这里的 MonadPlus 究竟是个什么东西……我们之后再说吧。
Monad In Java(大雾
Java 8 紧跟时髦,添加了 Optional(即 Maybe)和 Stream(类似列表)这两种 Monad,其中 bind 操作被命名为 flatmap,其的使用类似 do 对应的原始代码。下面的代码展示了 Optional Monad 的使用。
1 |
|
而下面的代码展示了 Stream Monad 和列表推导式的等价。
1 |
|
路长的很,不能膨胀。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!