封面来源:由博主个人绘制,如需使用请联系博主。

本文是 skopylov58/monads-in-pictures-in-java 中文译文,最初的原文是 Haskell 版本的 Functors, Applicatives, And Monads In Pictures,希望本文能对不懂 Haskell 的中文读者有所帮助。

本文并未进行逐词逐句地翻译,而是在尽量保证语义不发生变化的情况下,使其更符合中文读者的阅读习惯。如有错误,希望各位看官不吝赐教,批评指正。

本文将使用 JShell 来演示代码片段:

1
2
3
4
5
C:\Users\Mofan>jshell
| 欢迎使用 JShell -- 版本 21.0.7
| 要大致了解该版本, 请键入: /help intro

jshell>

注意:JShell 是 Java 9 新增的交互式脚本工具,请保证使用的 Java 版本符合要求。

这是一个简单的值:

value

我们知道如何将一个「函数」应用到这个值:

value_apply

在 JShell 中,它看起来像这样:

1
2
3
4
5
jshell> Function<Integer, Integer> add3 = x -> x + 3
add3 ==> $Lambda/0x000002d83a00a1f8@5ec0a365

jshell> var res = add3.apply(2)
res ==> 5

这很简单。

我们可以将其扩展为:任何值都可以出现在「上下文」(Context)中。目前你可以将「上下文」想象成一个可以放入值的盒子:

value_and_context

当你将一个函数应用于这个值时,你将 依赖于上下文 而得到不同的值。Functors、Applicatives、Monads,Arrows 等概念都是基于这种思想。

Maybe 数据类型定义了两种相关的上下文:

context

Java 中没有 Maybe 数据类型,可以用 Optional<T> 来代替它,用于模拟值 T 的缺失。

可以参考下表将 Haskell 中的一些概念用 Java 类进行替代:

Haskell Java
Maybe Optional<T>
Just 2 Optional.of(2)
Nothing Optional.empty()
1
2
3
4
5
jshell> var just2 = Optional.of(2)
just2 ==> Optional[2]

jshell> var nothing = Optional.<Integer>empty()
nothing ==> Optional.empty

稍后我们将看到当某物是 Just a 而不是 Nothing 时,应用一个函数会有怎样的不同。首先让我们来聊聊 Functors!

1. Functors

Functor,函子。

当值被封装到上下文中时,不能对其应用一个普通函数:

no_fmap_ouch

这就是 map 发挥作用的地方(在 Haskell 中是 fmap)。

map 是混江湖的,map 对上下文门儿清(感谢 DeepSeek 给出的译文参考)。它懂得如何将函数应用于封装在上下文中的值。

假设你想将 add3 应用于 just2,使用 map 吧:

1
2
jshell> var just5 = just2.map(add3)
just5 ==> Optional[5]

fmap_apply

哇哦!map 向我们展示了它是如何做到的!但 map 是怎么知道应该怎么应用这个函数的呢?

2. 究竟什么是 Functor?

Functor 是一个抽象基类(在 Haskell 中叫 typeclass)。

Haskell 中的定义如下:

functor_def

在 Java 中,它可以是这样的一个函数式接口:

1
2
3
4
@FunctionalInterface
public interface Functor<T> {
<R> Functor<R> map(Function<T, R> mapper);
}

Functor 是一个定义了 map 如何应用于他的数据类型。

Haskell 中 fmap 的工作原理如下:

fmap_def

所以在 Java 里可以这样做:

1
2
jshell> just2.map(x -> x + 3)
$6 ==> Optional[5]

map 神奇地应用了这个函数,因为 Optional 是一个 Functor。它指定 了 map 如何应用于空的和非空的 Optional

当我们写下 just2.map(x -> x + 3) 时,背后发生的事情是这样的:

fmap_just

所以你可能会想,好吧 map,请将 x -> x + 3 应用到 Nothing

1
2
jshell> nothing.map(x -> x + 3)
$7 ==> Optional.empty

bill

比尔・奥莱利(Bill O’Reilly )对 Maybe 函子一无所知

就像《黑客帝国》中的墨菲斯[1]一样,map 知道该怎么做;从 Nothing 开始,最后得到的也是 Nothingmap 颇具禅意。现在明白了为什么会存在 Optional 数据类型。例如,在没有 Optional 数据类型的语言中,你会这样处理数据库记录:

1
2
3
4
5
6
var post = Post.find_by_id(1);
if (post != null) {
return post.getTitle();
} else {
return "N/A";
}

但在 Java 中使用 Optional

1
2
3
return Optional.ofNullable(Post.find_by_id(1))
.map(Post::getTitle)
.orElse("N/A")

如果 find_by_id() 返回了一个帖子,我们将用 getTitle() 获取其标题;如果它返回 Nothing,我们也将返回 Nothing。挺酷的,对吧?

这里还有另一个例子:当你将一个函数应用于一个列表会发生什么呢?

fmap_list

在 Java 中,可以使用 List#stream() 方法将 List 对象转换为 Stream 对象,也可以直接构建包含指定元素的 Stream 对象。

Stream 也是函子,在 Java 中有:

1
2
jshell> Stream.of(2,4,6).map(add3).toList()
$8 ==> [5, 7, 9]

好的,好的,再举一个例子:当你把一个函数应用到另一个函数上时又会发生什么呢?

这是一个函数:

function_with_value

然后将一个函数应用到另一个函数上:

fmap_function

结果依旧是一个函数!

1
2
3
4
5
jshell> var add5 = add3.andThen(x -> x + 2)
add5 ==> java.util.function.Function$$Lambda/0x000002a98105c070@6ddf90b0

jshell> add5.apply(10)
$10 ==> 15

因此函数也是函子!

3. Applicatives

Applicative,适用函子(应用函子)。

它是 Functor 的扩展类型,除了具备 Functor 的功能外,还能处理「函数位于上下文中」的情况,支持将封装在上下文中的函数应用到封装的值上。

使用 Applicative 时,我们的值会被封装在一个上下文中,就像 Functor 一样:

value_and_context

但我们的函数也能被封装在一个上下文中!

function_and_context

好的,先让这个概念沉淀一下。Applicatives 不会开玩笑。它知道如何将一个封装在上下文中的函数应用到一个封装在上下文中的值:

applicative_just

Java 中的 Optional 并不是 Applicatives,但我们可以为它编写一个 aplicative() 函数:

1
2
3
<T, R> Optional<R> applicative(Optional<Function<T, R>> func, Optional<T> value) {
return func.flatMap(f -> value.flatMap(v -> Optional.ofNullable(f.apply(v))));
}
1
2
3
4
jshell> <T, R> Optional<R> applicative(Optional<Function<T, R>> func, Optional<T> value) {
...> return func.flatMap(f -> value.flatMap(v -> Optional.ofNullable(f.apply(v))));
...> }
| 已创建 方法 applicative(Optional<Function<T, R>>,Optional<T>)

让我们来测试一下:

1
2
3
4
5
6
7
8
jshell> applicative(Optional.of(add3), just2)
$12 ==> Optional[5]

jshell> applicative(Optional.empty(), just2)
$13 ==> Optional.empty

jshell> applicative(Optional.of(add3), nothing)
$14 ==> Optional.empty

在 Haskell 中使用 <*> 可能会导致一些有趣的情况,比如:

applicative_list

Java 里并没有 <*> 运算符,但可以使用以下方式实现相同的效果:

1
2
3
4
5
jshell> Function<Integer, Integer> mult2 = x -> 2 * x
mult2 ==> $Lambda/0x000002a98100dc50@3a5ed7a6

jshell> Stream.of(mult2, add3).flatMap(f -> Stream.of(1,2,3).map(f)).toList()
$16 ==> [2, 4, 6, 4, 5, 6]

这里有一些可以用 Applicatives 做到、但用 Functors 做不到的事情。如何将一个接收两个参数的函数应用于两个被封装的值?

Applicatives 将 Functors 推到一边,「大男孩可以使用任意参数数量的函数」,它说,「凭借 Haskell 中的 <$><*>,我可以处理接收任意数量未封装值的任意函数,然后将所有封装的值都传递给这个函数,最后得到一个封装值!哈哈哈哈!」。

而且还有一个名为 lift_a2 的方法也能做到相同的事(Java 无法处理 aplicative 调用中任意数量的参数,但我们可以创建一个类似的 lift_a2 方法):

1
2
3
<A, B, C> Optional<C> lift_a2(Optional<A> optA, Optional<B> optB, BiFunction<A, B, C> bifunc) {
return optA.flatMap(a -> optB.flatMap(b -> Optional.of(bifunc.apply(a, b))));
}

该方法接收两个封装的值(使用 Optional 封装),还有一个接收两个未封装值的 bifunc 函数:

1
2
3
4
jshell> <A, B, C> Optional<C> lift_a2(Optional<A> optA, Optional<B> optB, BiFunction<A, B, C> bifunc) {
...> return optA.flatMap(a -> optB.flatMap(b -> Optional.of(bifunc.apply(a, b))));
...> }
| 已创建 方法 lift_a2(Optional<A>,Optional<B>,BiFunction<A, B, C>)

让我们测试一下:

1
2
3
4
5
jshell> lift_a2(just2, Optional.of(3), (x, y) -> x * y)
$18 ==> Optional[6]

jshell> lift_a2(just2, nothing, (x, y) -> x * y)
$19 ==> Optional.empty

4. Monads

Monad,单子。

它进一步拓展了 Applicative,用于处理嵌套上下文。

如何学习 Monad:

  1. 获得计算机科学博士学位。
  2. 扔掉它,因为你不需要它来学习这一部分!

Monad 增加了新的变化。

Functor 能够将一个函数应用于被封装的值:

fmap

Applicative 能够将一个被封装的函数应用于一个被封装的值:

applicative

Monad 则能够将一个返回封装的值的函数应用于一个被封装的值。Monad 有一个 flatMap 函数(在 Haskell 中是 >>=,发音为 bind)来完成这个操作。

让我们来看一个例子。经典的 Maybe 数据类型就一个 Monad:

context

假设 half 是一个仅适用于偶数的函数:

1
Function<Integer, Optional<Integer>> half = i -> i % 2 == 0 ? Optional.of(i / 2) : Optional.empty();
1
2
3
jshell> Function<Integer, Optional<Integer>> half = i -> i % 2 == 0 ? Optional.of(i / 2) : Optional.
empty()
half ==> $Lambda/0x000002b01500f740@131276c2

当传入 half 函数的参数是偶数时,返回由 Optional 包装的一半的值,否则返回 Optional.empty(),即 Nothing

如果传入 half 函数的参数是一个被封装的值:

half_ouch

half 函数无法正常工作。

我们需要使用 flatMap(在 Haskell 中是 >>=)将封装后的值传入函数中。这是 flatMap 的照片 🤣:

plunger

它的工作原理如下:

1
2
3
4
5
6
7
8
jshell> Optional.of(4).flatMap(half)
$21 ==> Optional[2]

jshell> Optional.of(2).flatMap(half)
$22 ==> Optional[1]

jshell> Optional.of(1).flatMap(half)
$23 ==> Optional.empty

这里面发生了什么?Monad 是另一个抽象基类(在 Haskell 中,它是另一个 typeclass)。这是它在 Java 中的部分定义:

1
2
3
public interface Monad<T> {
<R> Monad<R> flatMap(Function<? super T, ? extends Monad<R>> mapper);
}

bind (在 Java 中是 flatMap,在 Haskell 中是 >>=)的含义是:

bind_def

因此 Java 中的 Optional<T> 实际上是一个 Monad。

Just 3 来演示下使用示例:

monad_just

1
2
jshell> Optional.of(3).flatMap(half)
$24 ==> Optional.empty

如果你传入一个 Nothing,那就更简单了:

monad_nothing

1
2
jshell> nothing.flatMap(half)
$25 ==> Optional.empty

你也可以链式调用它们:

monad_chain

1
2
jshell> Optional.of(20).flatMap(half).flatMap(half).flatMap(half)
$26 ==> Optional.empty

whoa

酷毙了!

在 Haskell 中,Maybe 既是一个 Functor,也是一个 Applicative,还是一个 Monad

但在 Java 中,Optional 只是一个 FunctorMonad,它不是 Applicative,但如果需要,可以提供一个 applicative 函数。

5. IO Monad

现在让我们来看另一个例子,IO Monad:

io

Java 标准库中并没有 IO Monad,为了翻译,我们需要完成一个最小的 IO Monad 实现:

1
2
3
4
5
6
7
8
public interface IO<T> {

T run();

default <R> IO<R> flatMap(Function<T, IO<R>> mapper) {
return () -> mapper.apply(run()).run();
}
}

它和 Supplier<T> 很类似,内部增加了一个 flatMap 方法。

具体来说有三个函数。getLine 不接受任何参数,直接获取用户的输入:

getLine

1
2
3
4
IO<String> getLine() {
// 模拟从控制台读取真实的输入
return () -> "read line";
}

readFile 接收一个字符串(文件名)并返回该文件的内容:

readFile

1
2
3
4
IO<String> readFile(String fileName) {
// 模拟从文件中读取内容
return () -> "file content";
}

putStrLn 接收一个字符串并打印它:

putStrLn

1
2
3
4
5
6
7
IO<Void> printLine(String text) {
return () -> {
// 这个不用模拟,这个简单 :)
System.out.println(text);
return null;
};
}

这三个函数都接收一个普通值(或者不接收值),并返回一个分装的值。我们可以使用 flatMap 将它们串联起来:

monad_io

1
2
3
var io = getLine().flatMap(s -> readFile(s)).flatMap(s -> printLine(s));

io.run();

哇哦!前排的各位请看 Monad 的表演!

Haskell 还为我们提供了一些关于 Monad 的语法糖,称为 do 表示法:

1
2
3
4
foo = do
filename <- getLine
contents <- readFile filename
putStrLn contents

可惜,这在 Java 中并没有。

6. 总结

  1. 一个 functor 是一种实现了 Functor 抽象基类的数据类型。
  2. 一个 applicative 是一种实现了 Applicative 抽象基类的数据类型。
  3. 一个 monad 是一种实现了 Monad 抽象基类的数据类型。
  4. Haskell 中的 Maybe 实现了全部三个,因此它既是 functor,也是 applicative,还是 monad;但 Java 中的 Optional<T> 只实现了 FunctorMonad

这三个有什么区别呢?

recap

  • functors:使用 map 将一个函数应用于一个被封装的值;
  • applicatives: 使用 applicative 将一个被封装的函数应用于一个被封装的值;
  • monads:使用 flatMap 将一个返回被封装的值的函数应用于一个被封装的值。

  1. 墨菲斯清楚虚拟与现实的规则,能精准指引尼奥应对未知。map 也是这样,它知道如何处理 Optional,有值就处理值,没值就保持原样,就像墨菲斯看穿规则一样,map 也看穿了 Optional 的操作逻辑,能够给出对应的应对方案。 ↩︎