1. 概述
本教程将深入探讨Monads的概念,以及如何利用它们处理副作用(side effects)。我们将重点介绍两个核心方法:map()
和flatMap()
,它们是链式操作Monads的关键。通过分析Java生态中几种主流Monads的API,我们将聚焦其实际应用场景。
2. 副作用处理
在函数式编程中,"副作用"通常指超出函数或组件作用域的操作。为了在保持函数纯性的同时处理这些副作用,我们可以将操作或数据包装进容器中。Monads本质上就是这类容器,能将副作用隔离在函数作用域之外。
举个简单例子,考虑一个整数除法函数:
double divide(int dividend, int divisor) {
return dividend / divisor;
}
看似纯函数,但当divisor
为0时会抛出ArithmeticException
产生副作用。我们可以用Monads封装结果来规避:
Optional<Double> divide(int dividend, int divisor) {
if (divisor == 0) {
return Optional.empty();
}
return Optional.of(dividend / divisor);
}
现在除零操作不再产生副作用。以下是Java中处理不同副作用的常见Monads:
-
Optional<>
– 处理空值问题 -
List<>
,Stream<>
– 管理数据集合 -
Mono<>
,CompletableFuture<>
– 处理并发和I/O -
Try<>
,Result<>
– 处理错误 -
Either<>
– 处理二元状态
3. 函子(Functors)
创建Monads时,需要允许其修改封装对象或操作,同时保持容器类型不变。以Java Stream为例:
若现实世界中Long
可通过Instant.ofEpochSeconds()
转为Instant
,在Stream世界中这种关系也应保持。**Stream API通过高阶函数"提升"这种关系,这就是函子概念,其转换方法通常命名为map()
**:
Stream<Long> longs = Stream.of(1712000000L, 1713000000L, 1714000000L);
Stream<Instant> instants = longs.map(Instant::ofEpochSecond);
虽然map
是典型命名,但具体方法名不影响函子本质。例如CompletableFuture
使用thenApply()
:
CompletableFuture<Long> timestamp = CompletableFuture.completedFuture(1713000000L);
CompletableFuture<Instant> instant = timestamp.thenApply(Instant::ofEpochSecond);
两种容器都暴露了方法,允许对封装数据应用其支持的所有操作:
4. 绑定(Binding)
绑定是Monads的核心特性,允许在Monadic上下文中链式组合多个计算。简单说,用绑定替代map()
可避免嵌套结构。
4.1. 嵌套Monads问题
若仅依赖函子序列化操作,最终会产生嵌套容器。我们用Project Reactor的Mono
演示:
假设有两个响应式方法获取Author
和Book
:
Mono<Author> findAuthorByName(String name) { /* ... */ }
Mono<Book> findLatestBookByAuthorId(Long authorId) { /* ... */ }
从作者名开始获取详情:
void findLatestBookOfAuthor(String authorName) {
Mono<Author> author = findAuthorByName(authorName);
// ...
}
若用map()
将容器内容从Author
转为Book
:
Mono<Mono<Book>> book = author.map(it -> findLatestBookByAuthorId(it.authorId());
结果产生嵌套的Mono
容器——因为findLatestBookByAuthorId()
返回Mono
,而map()
又包装了一层。
4.2. flatMap()
解决方案
使用绑定(通常叫flatMap
)可消除额外容器,扁平化结构:
void findLatestBookOfAuthor(String authorName) {
Mono<Author> author = findAuthorByName(authorName);
Mono<Book> book = author.flatMap(it -> findLatestBookByAuthorId(it.authorId()));
// ...
}
进一步优化,内联操作并添加中间map()
提取authorId
:
void findLatestBookOfAuthor(String authorName) {
Mono<Book> book = findAuthorByName(authorName)
.map(Author::authorId)
.flatMap(this::findLatestBookByAuthorId));
// ...
}
组合map()
和flatMap()
是操作Monads的高效方式,能以声明式定义转换链。
5. 实战场景
Monads通过抽象层帮我们处理副作用。多数情况下,它们让我们聚焦主流程,将异常处理隔离在主逻辑之外。
5.1. "铁路轨道"模式
Monads绑定也叫"铁路轨道"模式。主流程像直行轨道,异常情况则切换到平行轨道。
以验证Book
对象为例:检查ISBN → 验证authorId
→ 校验书籍类型:
void validateBook(Book book) {
if (!validIsbn(book.getIsbn())) {
throw new IllegalArgumentException("Invalid ISBN");
}
Author author = authorRepository.findById(book.getAuthorId());
if (author == null) {
throw new AuthorNotFoundException("Author not found");
}
if (!author.genres().contains(book.genre())) {
throw new IllegalArgumentException("Author does not write in this genre");
}
}
用Vavr的Try
monad实现铁路轨道模式链式验证:
void validateBook(Book bookToValidate) {
Try.ofSupplier(() -> bookToValidate)
.andThen(book -> validateIsbn(book.getIsbn()))
.map(book -> fetchAuthor(book.getAuthorId()))
.andThen(author -> validateBookGenre(bookToValidate.genre(), author))
.get();
}
void validateIsbn(String isbn) { /* ... */ }
Author fetchAuthor(Long authorId) { /* ... */ }
void validateBookGenre(String genre, Author author) { /* ... */ }
API暴露andThen()
等无需响应的方法,用于检测失败并切换轨道。而map()
/flatMap()
则推进流程,创建新Try<>
包装响应(如Author
对象):
5.2. 故障恢复
某些API允许从失败轨道恢复到主轨道,通常需要提供回退值。例如Try<>
的recover()
方法:
5.3. 其他案例
理解Monads工作原理后,具体方法名并不重要,关键是理解其目的。Monads API方法通常分为两类:
- 转换底层数据
- 必要时切换轨道
例如:
Optional
用map()
/flatMap()
转换数据,用filter()
/or()
切换"空/存在"状态CompletableFuture
用thenApply()
/thenCombine()
替代map()
/flatMap()
,通过exceptionally()
恢复失败轨道
6. 总结
本文探讨了Monads的核心特性,通过实例演示了如何处理集合、并发、空值、异常等副作用。我们学习了"铁路轨道"模式绑定Monads,将副作用隔离在组件作用域之外。
完整源码请参考GitHub仓库。