使用 Monad 的>>=实现<$>和<*>

惊为天人,惊为天人啊,没想到仅使用 Monad 的>>=return便可以实现<$><*>!下面描述一下我的心路历程。

在一切发生之前,先来梳理一下我们手头的工具。我们只能使用 Monad 中直接定义的方法,即——

1
2
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a

就这些!开始表演魔法吧。

<$>

首先是<$>fmap,先看其签名——

1
fmap :: (a -> b) -> m a -> m b

在这里,函数a -> b和变量m a就是我们的已知量,我们要用这两个素材得到m b。容易意识到,我们只需要对函数a -> b进行变换,根据这个函数构造一个函数a -> m b即可,这样得到的函数签名刚好就是flip (>>=)

而如何进行这种构造?整个 lambda,在 lambda 上下文中调用函数a -> b并返回一个m b即可!

1
2
magic :: Monad m => (a -> b) -> (a -> m b)
magic fn = \a -> return $ fn a

这个形式可以化简,但为了明确,这里不化简。

有了这个 magic,我们就能够构造 fmapM 了!

1
2
fmapM :: Monad m => (a -> b) -> m a -> m b
fmapM fn ma = (>>=) ma $ magic fn

<*>

<*>的签名如下——

1
(<*>) :: Monad m => m (a -> b) -> m a -> m b

这里,最关键的问题是,该如何把这个a -> b从 Monad 的上下文里拿出来(否则无法进行使用!)。

显然,a -> b(以及任何其它变量)是不能直接赤果果直接放到“全局作用域”的——它们在上下文中,只能通过模式匹配或 getter 拿出来,甚至对于 IO,这些手段也是无效的。

但是它又确实是能拿出来的……之前一切的学习都没有否定这一点…这就意味着,把它拿出来必须要有一定条件,也就是说,要在一定上下文中!显然,离开具体的数据类型,我们能构造的唯一的上下文就是 lambda 了!

于是我们又开始重新查看手头的工具,好像(>>=)给我们提供了这样一个上下文!再次查看(>>=)的签名——

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

我们看到了什么?m a中的值a(a -> m b)函数中被进行应用了!

(>>=)能提供我们这样的上下文!把值取出来,进行特定操作,再放回上下文中!

比如,我们有一个Just 1,我们想给它加 1,于是我们可以这样用——

1
res = (Just 1) >>= (\a -> return $ a + 1)

然后我们查看 fmap 的签名,好像 fmap 也能干这事!

1
res = (\a -> a + 1) <$> (Just 1)

于是破案了,纠结这么久,真实原因是我太傻,对 lambda 的意义了解不够深刻,遇上复杂问题就抓瞎了!

带着我们的新思路,再次查看我们所要实现的东西——

1
<*> :: Monad m => m (a -> b) -> m a -> m b

显然,通过上面的工具,我们能在 lambda 里把m (a -> b)中的a -> b解出来,比如这样——

1
2
allApp :: Monad m => m (a -> b) -> m a -> m b
allApp mf ma = mf >>= (\f -> _)

这里的 f 就是a -> b!在这个 lambda 的上下文里,我们有a -> b,有m a,显然这里该用fmap了!

1
allApp mf ma = mf >>= (\f -> fmap f ma)

BINGO!

但是稍微再瞅瞅呢?我们把 fmap 重新展开成使用>>=return构造的形式(以及删掉一些括号)——

1
2
3
4
allApp :: Monad m => m (a -> b) -> m a -> m b
allApp mf ma = mf >>= \f
-> ma >>= \a
-> return $ f a

这为什么合法?加点括号——

1
2
3
4
allApp :: Monad m => m (a -> b) -> m a -> m b
allApp mf ma = mf >>= (\f
-> (ma >>= \a
-> (return $ f a)))

(ma >>= \a -> (return $ f a))先执行,返回一个m b,结果直接作为(\f -> _)的返回值。真正的计算已经结束(如果有Nothing之类的,那就根本没有计算),得到的值会一直往外传递,最终作为allMap函数的返回值。

显然,这里存在着某种模式——我们可以同时将多个 Monad 中的值取出来并进行操作,最后再封装成新的 Monad。

使用这个思想,我们重新实现一下 fmap——

1
2
3
fmapM' :: Monad m => (a -> b) -> m a -> m b
fmapM' fn ma = ma >>= \a
-> return $ fn a

清晰!

一些更酷的东西

更酷(或许没有再酷了!)的是,do 语法糖是可以翻译成>>>>=的!见下面的代码——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
main = do
putStrLn "input value 1"
val1 <- readInt
putStrLn "input value 2"
val2 <- readInt
let res = val1 + val2
let doubleRes = res * 2
print $ mconcat ["2 * (", show val1," + ",show val2, ") = ", show doubleRes]

-- 特意组织成这样的结构,使其映射更为清晰
main' =
putStrLn "input value 1" >>
readInt >>= \val1 ->
putStrLn "input value 2" >>
readInt >>= \val2 ->
let res = val1 + val2 in
let doubleRes = 2 * res in
print $ mconcat ["2 * (", show val1," + ",show val2, ") = ", show doubleRes]

当我们要副作用的时候,我们使用>>,当我们要从一个 Monad 里取出值的时候,我们使用>>=,do 就是这样!

另一个很酷的地方是,列表推导式也是可以通过 do 进行描述的!比如下面的两个使用是等价的——

1
2
3
4
5
6
7
8
someList = do
val1 <- [1 .. 10]
val2 <- [1..10]
if val1 * val2 < 50
then [val1 * val2]
else []

someList' = [val1 * val2 | val1 <- [1..10], val2 <- [1..10], val1 * val2 < 50]

令人感叹,令人感叹啊。再次感叹 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
2
3
4
Nothing >> Just 1 -- Nothing
Just 1 >> Just 2 -- Just 2
[] >> [1,2,3] -- []
[1] >> [1,2,3] -- [1,2,3]

显然,当前者为 mempty(实际上是 mzero)的时候,值为后者,否则为 mempty。

因此,我们可以试图定义自己的 guard——

1
2
3
4
5
6
7
8
9
guard' :: MonadPlus m => Bool -> m ()
guard' True = return ()
guard' False = mzero

abc :: [Integer]
abc = do
i <- [1..100]
guard' $ i `mod` 5 == 0
return i

至于这里的 MonadPlus 究竟是个什么东西……我们之后再说吧。

Monad In Java(大雾

Java 8 紧跟时髦,添加了 Optional(即 Maybe)和 Stream(类似列表)这两种 Monad,其中 bind 操作被命名为 flatmap,其的使用类似 do 对应的原始代码。下面的代码展示了 Optional Monad 的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 关于 Optional 的使用
public class Main {
public static final Object Unit = new Object();
public static <K, V> Optional<V> getMaybe(Map<K, V> map, K k) {
if (map.containsKey(k))
return Optional.of(map.get(k));
return Optional.empty();
}
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>() {{
put("hello", 1);
put("world", 2);
}};
/*
“等价”于——
do
println "hello! Optional Monad!" -- 这显然不是合法的 Haskell 代码
a <- getMaybe "hello"
println a
b <- getMaybe "world"
println b
return a + b
*/
Optional<Integer> res =
Optional.of(Unit).flatMap(__ -> {
System.out.println("hello! Optional Monad!");
return getMaybe(map, "hello").flatMap(a -> {
System.out.println(a);
return getMaybe(map, "world").flatMap(b -> {
System.out.println(b);
return Optional.of(a + b);});});}); -- 这一堆分号有点恐怖!如果是在 kotlin 里,这代码会干净漂亮很多,只有花括号了
System.out.println(res);
}
}

而下面的代码展示了 Stream Monad 和列表推导式的等价。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
假设我们要求 [z * z | x <- [1 .. 100], y <- [1 .. 50],
let z = x + y, ord x, even y]
为方便理解,先转换成 do 形式
do
x <- [1 .. 100]
y <- [1 .. 50]
let z = x + y
guard $ ord x && even y
return $ z * z
再转换成原始形式
*/
IntStream.range(1, 101).flatMap(x ->
IntStream.range(1, 51).flatMap(y -> {
int z = x + y;
if (x % 2 == 1 && y % 2 == 0)
return IntStream.of(z * z);
return IntStream.empty();
}));

路长的很,不能膨胀。