Monad不就是个自函子范畴上的幺半群,这有什么难理解的。 Phillip Wadler
当我们谈论Monad的时候,我们在谈论什么
坊间一直流传着一句话:“一百个学FP的人的心中就有一百个对Monad的理解”。而我相信,他们中的大部分人在看明白后又会写出一篇崭新的Monad文。我也一直很想写一写自己关于Monad的见解,但是一直找不到合适的说明方式。先前我在某群提到,从Optional(也就是Haskell的Maybe)理解Monad会是一个很不错的方式。而直到最近我正好看到了这样一篇文章(Reference 1),与我的想法不谋而合,于是我就借用这篇文章的方式谈一谈我对Monad的理解吧。
Monad作为函数式编程中最著名的几个输出概念之一,困扰了一批又一批想要学习的工程型选手。在我看来,主要的理解障碍有二:
- 定义晦涩难懂,一介绍Monad就要从Functor、Applicative一字排开。而大部分语言浅显的文章又“绕着Monad转”,就是不说Monad是什么
- 无法直观的看出Monad的用处。废了老大劲看完的文章,也不知道Monad能干嘛,看了几个示范的Monad又仿佛Monad什么都能干
综上,我打算用工程化的方式来解释Monad到底是什么。之后,用Haskell作为过渡,最后在讲讲理论相关的内容。而第一篇作为工程部分,自然用的是大家最喜欢的Java主要是我最喜欢来讲解了。
不过我先打个预防针,本篇文章是站在工程角度的浅显介绍,因此语言可能不甚严谨。
Monad是层数很高的抽象
和Runnable
一样,Monad是一个功能的抽象。在Java中,我们可以用接口类来描述它。就像你说Thread
是Runnable
一样,我们也同样可以说XX类
是Monad
。实现了Monad要求的方法,你就可以用一些公用的方法来操作一个类了,就这么简单。
唯一的难点是,Monad要求实现的方法没有特定的功能。这比较像Comparable
,而我们知道Comparable
比较大小的语义纯粹只是人为增加的而已。只要符合一些规则(自反性、反对称性、传递性),你就可以编写一个靠谱的Comparable
。Monad也一样,只不过Monad更加抽象。Monad这波绝对不止第五层
Functor
知道了Monad就是个抽象,下一步就是看看它要求什么接口、能帮我们干什么。于是这就避不开Functor了,因为Functor实际上是Monad的一个“初级形态”。不妨直接来看看Functor的一个Java定义:
代码语言:javascript复制import java.util.function.Function;
public interface Functor<T> {
<R> Functor<R> map(Function<T, R> f);
}
可以看到Functor是一个泛型类,接受一个泛型参数T。此外,Functor接口只需要实现一个map方法。这个map方法接受一个函数,它的参数类型为T,返回值类型为R,写作T -> R
。此外,调用时我们还传入了Functor<T>
类型的this
。最后,函数返回了Functor<R>
。
既然明白了这个要求,我们就来举个例子看看Functor到底在抽象什么。
代码语言:javascript复制import java.util.function.Function;
public class MyFunctor<T> implements Functor<T> {
private final T value;
public MyFunctor(T value) {
this.value = value;
}
@Override
public <R> MyFunctor<R> map(Function<T, R> f) {
final R result = f.apply(value);
return new MyFunctor<R>(result);
}
}
简而言之,MyFunctor<T>
就是一个T类型的容器,然后map
就把参数的函数f
应用到自己的身上,得到一个MyFunctor<R>
。有什么用呢?嘛,这样你就可以写:
new MyFunctor<>("Functor gives fmap")
.map((String s) -> s.substring(0, 3))
.map(String::toLowerCase)
.map(String::getBytes);
好像根本没有什么锤子用,看起来我们只是把所有值的外边套了一个MyFunctor
的娃,然后把一次次调用放在了map函数里。这种什么都不干,套了娃跟没套一样的Functor有一个名字:Identity。
虽然Identity好像没什么用,但是看到这个代码,不觉得有内味了嘛?什么味?这不就是Optional API的调用方法嘛!没错,稍微改一改,我们就能得到一个Optional。
代码语言:javascript复制import java.util.function.Function;
public class MyOptional<T> implements Functor<T> {
private final T valueOrNull;
private MyOptional(T valueOrNull) {
this.valueOrNull = valueOrNull;
}
@Override
public <R> MyOptional<R> map(Function<T, R> f) {
if (valueOrNull == null)
return empty();
else
return of(f.apply(valueOrNull));
}
public static <T> MyOptional<T> of(T a) {
return new MyOptional<T>(a);
}
public static <T> MyOptional<T> empty() {
return new MyOptional<T>(null);
}
}
虽然调用的格式还是和Identity一样,只不过现在链式调用是完全空安全的。看到这里,Functor抽象的东西应该就很明显了:一个容器。而map抽象的,则是对容器内部值的操作。
而且由于Functor的抽象层数很高,因此它能抽象Optional这种有两个状态的容器。当然,抽象List这种不限长度的容器也是OK的。
代码语言:javascript复制import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.function.Function;
public class FList<T> implements Functor<T, FList<?>> {
private final ImmutableList<T> list;
public FList(Iterable<T> value) {
this.list = ImmutableList.copyOf(value);
}
@Override
public <R> FList<R> map(Function<T, R> f) {
ArrayList<R> result = new ArrayList<R>(list.size());
for (T t : list) {
result.add(f.apply(t));
}
return new FList<>(result);
}
}
这样,我们就可以方便的对列表元素进行链式操作了。
Monad
但是Functor还是有一个问题,它没法解决嵌套。比如,如果我们希望计算两个MyOptional<Integer>
的和,得到一个MyOptional<Integer>
,那要怎么编码呢?
MyOptional<Integer> optA = MyOptional.of(1);
MyOptional<Integer> optB = MyOptional.of(2);
var result = optA.map(a -> optB.map(b -> a b));
我们虽然不能确定optA
和optB
内部的值(它们可能是null
),但是通过map,我们可以变相的得到他们的真实值。这么写倒是没什么问题,但是,你猜猜result
的类型是什么?没错,是:MyOptional<MyOptional<Integer>>
。而且随着你需要的参数变多(这里是加法,故只需要两个),结果的套娃也会一层一层变多。这太丑陋了!
而且你细品这个娃品谁的娃?。你会发现内层MyOptional
实际上是因为我们无法确定optB
有无值而引入的。但是实际上我们希望达到的效果是,只要optA
或optB
有一个是empty的就返回MyOptional.empty()
,否则就计算加法。因此,这个娃套的可以说完全没有必要。
Monad就是对这种没必要的套娃的抽象。Monad引入了一个join函数,把两层嵌套简化为一层:
代码语言:javascript复制import java.util.function.Function;
public abstract class Monad<T> implements Functor<T> {
public abstract <R> Monad<R> map(Function<T, R> f);
public abstract <R> Monad<R> join(Monad<Monad<R>> m);
}
这里注意,join
本来的意图是将Monad<Monad<R>>
变成Monad<R>
,因此理论上它应该是个静态方法。但是Java不允许声明抽象静态方法,只能变成这样了。
很明显,对于Optional实现join
,我们只要检查内部值是不是null
就可以安心返回内层Optional了。也就是:
@Override
public <R> MyMonadOptional<R> join(Monad<Monad<R>> m) {
var opt = (MyMonadOptional<Monad<R>>) m;
if (opt.valueOrNull == null)
return empty();
else
return (MyMonadOptional<R>) opt.valueOrNull;
}
这样我们只需要在调用的最后增加一个join
,就可以消除这层没必要的嵌套了。
不光如此,有了join
函数,我们还可以很简单的构造出flatMap
函数。
public <R> Monad<R> flatMap(Function<T, Monad<R>> f) {
Monad<Monad<R>> result = this.map(f);
return this.join(result);
}
在Optional的情况下,flatMap
是用来实现返回值本身可能是null
的函数,比如:
MyMonadOptional<Integer> tryParse(String s) {
try {
final int i = Integer.parseInt(s);
return MyMonadOptional.of(i);
} catch (NumberFormatException e) {
return MyMonadOptional.empty();
}
}
使用flatMap
,我们同样可以避免map
会产生的嵌套问题。还记得开始的时候我们举得例子嘛?我们现在可以改写成:
var mOptA = MyMonadOptional.of(1);
var mOptB = MyMonadOptional.of(2);
var ret = mOptA.flatMap(a -> mOptB.map(b -> a b));
// ret is MyMonadOptional<Integer> !!
更有意思的一件事情是,使用flatMap
也可以实现join
函数。也就是说,我们也能定义出Monad!
import java.util.function.Function;
public abstract class Monad<T> implements Functor<T> {
public abstract <R> Monad<R> map(Function<T, R> f);
public abstract <R> Monad<R> flatMap(Function<T, Monad<R>> f);
public <R> Monad<R> join(Monad<Monad<R>> m) {
return m.flatMap(Function.identity());
}
}
而这个定义,就是大多数编程语言(比如Scala、Haskell)对Monad的定义。
Program in Monad
通过了刚才的介绍,你应该能找出不少现有的Monad。像一直举例用的Java的Optional API,Java的Stream API,还有ES6的Promise,它们本质上其实都是Monad。相信这些Monad你应该已经不难看出了,所以我想介绍一个略微特别的Monad——列表。
由于我们之前已经实现过列表的Functor了,因此我们只需要考虑它的join
,也就是要设计一个把嵌套的列表变成不嵌套的函数。嘛,直接把他们连起来就可以了。比如:join([ [ 1, 2 ], [ 3, 4 ] ]) = [ 1, 2, 3, 4 ]
。
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.function.Function;
public class MonadList<T> extends Monad<T> {
private final ImmutableList<T> list;
public MonadList(Iterable<T> value) {
this.list = ImmutableList.copyOf(value);
}
@Override
public <R> MonadList<R> map(Function<T, R> f) {
var result = new ArrayList<R>(list.size());
for (T t : list) {
result.add(f.apply(t));
}
return new MonadList<>(result);
}
@Override
public <R> MonadList<R> join(Monad<Monad<R>> m) {
var lists = (MonadList<Monad<R>>) m;
var result = new ArrayList<R>(list.size());
for (var list : lists.list) {
result.addAll(((MonadList<R>) list).list);
}
return new MonadList<>(result);
}
}
接下来,我们看看对Monad开发的通用函数用在List身上是什么效果。还记得我们之前计算加法的模式嘛?稍微抽象下,我们就能得到一个对所有Monad都适用的函数:
代码语言:javascript复制import java.util.function.BiFunction;
public class Functional {
public static <T1, T2, R> Monad<R> liftM2(Monad<T1> t1, Monad<T2> t2, BiFunction<T1, T2, R> f) {
return t1.flatMap((T1 tv1) ->
t2.map((T2 tv2) -> f.apply(tv1, tv2))
);
}
}
那么问题来了。它用在List上是什么效果呢?
代码语言:javascript复制var lstA = new MonadList<>(Arrays.asList(1, 2));
var lstB = new MonadList<>(Arrays.asList(4, 5));
var result = Functional.liftM2(lstA, lstB, Integer::sum);
// [ 5, 6, 6, 7 ]
没错,它返回了4个数字,而这正是两个列表内容的所有可能组合进行运算的结果(1 4、1 5、2 4、2 5)!liftM2
作用于List的效果就是一个笛卡尔积。而且你细品,这不就是列表推导式嘛。
根据这个例子,不难看出:由于高度的抽象,基于Monad编写的函数(如liftM2
)本身没有“明确的用途”。根据Monad的不同,它实际表现出来的作用很可能相当不同。我觉得代码复用的最高层次也莫过于此。
不确定性之盒
说了那么多,你应该能了解到Monad只是一个抽象结构而已。不过这么说确实还是太抽象了,所以我打算用一个经典的例子再次描述Monad——纸箱。
由于需要一个类型参数T,Monad几乎必然持有一个T类型的值(你确实可以写一个完全不持有的Monad,但是它什么都做不了)。但是这个T类型的值存在的“形式”是不确定的。对于Optional来说,它有可能存储一个T,也有可能是空的。对List来说,它储存的T元素数目是不确定的。所以Monad实际上是一个存储不确定性的纸箱。
对于Monad,如果我们不“打开”它,我们就永远无法得知其中的内容,因为我们根本无法确定纸箱里面内容的具体形式。Monad的创意是,它用map来变相帮助我们读取它的内容!也就是说,Monad把处理数据的操作也变得不确定了。如果纸箱里有东西,我们就把它取出来处理,如没有东西就原封不动。操作的执行与否和纸箱里面的东西存在与否息息相关!而且Monad还允许我们引入新的不确定性。如果一个操作的结果就是一个纸箱,我们就不必再重复套纸箱了。
Monad的灵魂就在于不拆开纸箱。对于Optional,我们尽可能晚的打开纸箱(也就是get等等消去Optional的方法),这样我们就不用担心处理过程中的不确定性会影响整个流程了。而对于Promise,我们根本没办法打开纸箱。回调成了我们读取数据的唯一办法,而这就是Monad能抽象的部分。
下一个话题
到这里,我关于Monad工程角度的介绍就结束了。我个人认为,只是理解Monad的用途是没有必要,也没有意义去看Monad背后的数学定义的。
不过只从工程角度理解Monad是远远不够的。文中没有提及flatMap需要遵守的规则,对Monad的定义也不太完备(缺少了return),也没有细究join和flatMap的互相实现。要真正理解Monad,理论上的内容同样是不可避免的。
下一篇文章,我将简单介绍Haskell中的Monad实现与一些有趣的Monad,作为过渡。再下一篇,我将从理论角度(主要是范畴论)介绍Monad。
Reference
- Functional Programming in Pure Java: Functor and Monad Examples(https://dzone.com/articles/functor-and-monad-examples-in-plain-java)