使用 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
4do
i <- [1, 2, 3]
guard $ i % 2 == 0
i在这里,
guard $ i % 2 == 0要么返回[()],要么返回[],因此整个代码可以理解为
1
2
3
4do
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 协议 ,转载请注明出处!