【注】本文译自:[Beautiful World of Monads – DEV Community](
https://dev.to/siy/beautiful-…)
让我从免责申明开始。从函数式编程的角度来看,上面的解释绝不是准确的或相对精确的。相同,我将重点解释的清晰和简略性上,以便让尽可能多的 Java 开发人员进入这个漂亮的世界。
几年前,当我开始深入研究函数式编程时,我很快发现有大量的信息,但对于简直齐全具备命令式背景的一般 Java 开发人员来说,简直无奈了解。现在,状况正在缓缓扭转。例如,有很多文章解释了例如根本的 FP 概念(参考:实用函数式 Java(PFJ)简介)以及它们如何实用于 Java。或解释如何正确应用 Java 流的文章。然而 Monads 依然不在这些文章的重点之外。我不晓得为什么会产生这种状况,但我会致力填补这个空白。
那么,Monad 是什么?
Monad 是……一种设计模式。就这么简略。这种设计模式由两局部组成:
- Monad 是一个值的容器。对于每个 Monad,都有一些办法能够将值包装到 Monad 中。
- Monad 为外部蕴含的值实现了“管制反转”。为了实现这一点,Monad 提供了承受函数的办法。这些函数承受与 Monad 中存储的类型雷同的值,并返回转换后的值。转换后的值被包装到与源值雷同的 Monad 中。
为了了解模式的第二局部,咱们能够看看 Monad 的接口:
interface Monad<T> {<R> Monad<R> map(Function<T, R> mapper);
<R> Monad<R> flatMap(Function<T, Monad<R>> mapper);
}
当然,特定的 Monad 通常有更丰盛的接口,但这两个办法相对应该存在。
乍一看,承受函数而不是拜访值并没有太大区别。事实上,这使 Monad 可能齐全管制如何以及何时利用转换性能。当您调用 getter 时,您心愿立刻取得值。在 Monad 转换的状况下能够立刻利用或基本不利用,或者它的利用能够提早。不足对外部值的间接拜访使 monad 可能示意甚至尚不可用的值!
上面我将展现一些 Monad 的例子以及它们能够解决哪些问题。
Monad 缺失值或 Optional/Maybe 的场景
这个 Monad 有很多名字——Maybe、Option、Optional。最初一个听起来很相熟,不是吗?好吧,因为 Java 8 Optional 是 Java 平台的一部分。
可怜的是,Java Optional 实现过于尊敬传统的命令式办法,这使得它的用途不大。特地是 Optional 容许应用程序应用 .get() 办法获取值。如果短少值,甚至会抛出 NPE。因而,Optional 的用法通常仅限于示意返回潜在的缺失值,只管这只是潜在用法的一小部分。
兴许 Monad 的目标是示意可能会失落的值。传统上,Java 中的这个角色是为 null 保留的。可怜的是,这会导致许多不同的问题,包含驰名的 NullPointerException。
例如,如果您冀望某些参数或某些返回值能够为 null,则应该在应用前查看它:
public UserProfileResponse getUserProfileHandler(final User.Id userId) {final User user = userService.findById(userId);
if (user == null) {return UserProfileResponse.error(USER_NOT_FOUND);
}
final UserProfileDetails details = userProfileService.findById(userId);
if (details == null) {return UserProfileResponse.of(user, UserProfileDetails.defaultDetails());
}
return UserProfileResponse.of(user, details);
}
看起来相熟吗?当然了。
让咱们看看 Option Monad 如何扭转这一点(为简洁起见,应用一个动态导入):
public UserProfileResponse getUserProfileHandler(final User.Id userId) {return ofNullable(userService.findById(userId))
.map(user -> UserProfileResponse.of(user,
ofNullable(userProfileService.findById(userId)).orElseGet(UserProfileDetails::defaultDetails)))
.orElseGet(() -> UserProfileResponse.error(USER_NOT_FOUND));
}
请留神,代码更加简洁,对业务逻辑的“烦扰”也更少。
这个例子展现了 monadic 的“管制反转”是如许不便:转换不须要查看 null,只有当值理论可用时才会调用它们。
“如果 / 当值可用时做某事”是开始不便地应用 Monads 的要害心态。
请留神,下面的示例保留了原始 API 的残缺内容。然而更宽泛地应用该办法并更改 API 是有意义的,因而它们将返回 Optional 而不是 null:
public Optional<UserProfileResponse> getUserProfileHandler4(final User.Id userId) {return optionalUserService.findById(userId).flatMap(user -> userProfileService.findById(userId).map(profile -> UserProfileResponse.of(user, profile)));
}
一些察看:
- 代码更简洁,蕴含简直零样板。
- 所有类型都是主动派生的。尽管并非总是如此,但在绝大多数状况下,类型是由编译器派生的 — 只管与 Scala 相比,Java 中的类型推断较弱。
- 没有明确的错误处理,而是咱们能够专一于“高兴日子场景”。
- 所有转换都不便地组合和链接,不会中断或烦扰次要业务逻辑。
事实上,下面的属性对于所有的 Monad 都是通用的。
抛还是不抛是个问题
事件并不总是如咱们所愿,咱们的应用程序生存在事实世界中,充斥苦楚、谬误和失误。有时咱们能够和他们一起做点什么,有时不能。如果咱们不能做任何事件,咱们至多心愿告诉调用者事件并不像咱们预期的那样进行。
在 Java 中,咱们传统上有两种机制来告诉调用者问题:
- 返回非凡值(通常为空)
- 抛出异样
除了返回 null 咱们还能够返回 Option Monad(见上文),但这通常是不够的,因为须要更多对于谬误的详细信息。通常在这种状况下咱们会抛出异样。
然而这种办法有一个问题。事实上,甚至很少有问题。
- 异常中断执行流程
- 异样减少了很多心理开销
异样引起的心理开销取决于异样的类型: - 查看异样迫使你要么在这里解决它们,要么在签名中申明它们并将麻烦转移到调用者身上
- 未经查看的异样会导致雷同级别的问题,但编译器不反对
不晓得哪个更差。
Either Monad 来了
让咱们先剖析一下这个问题。咱们想要返回的是一些非凡值,它能够是两种可能的事件之一:后果值(胜利时)或谬误(失败时)。请留神,这些货色是互相排挤的——如果咱们返回值,则不须要携带谬误,反之亦然。
以上是对 Either Monad 的简直精确形容:任何给定的实例都只蕴含一个值,并且该值具备两种可能类型之一。
任何 Monad 的接口都能够这样形容:
interface Either<L, R> {<T> Either<T, R> mapLeft(Function<L, T> mapper);
<T> Either<T, R> flatMapLeft(Function<L, Either<T, R>> mapper);
<T> Either<L, T> mapLeft(Function<T, R> mapper);
<T> Either<L, T> flatMapLeft(Function<R, Either<L, T>> mapper);
}
该接口相当简短,因为它在左右值方面是对称的。对于更窄的用例,当咱们须要传递胜利或谬误时,这意味着咱们须要就某种约定达成统一——哪种类型(第一种或第二种)将保留谬误,哪种将保留值。
在这种状况下,Either 的对称性质使其更容易出错,因为很容易无心中替换代码中的谬误和胜利值。
尽管这个问题很可能会被编译器捕捉,但最好为这个特定用例量身定制。如果咱们修复其中一种类型,就能够做到这一点。显然,修复谬误类型更不便,因为 Java 程序员曾经习惯于从单个 Throwable 类型派生所有谬误和异样。
Result Monad — 专门用于错误处理和流传的 Either Monad
所以,让咱们假如所有谬误都实现雷同的接口,咱们称之为失败。当初咱们能够简化和缩小接口:
interface Result<T> {<R> Result<R> map(Function<T, R> mapper);
<R> Result<R> flatMap(Function<T, Result<R>> mapper);
}
Result Monad API 看起来与 Maybe Monad 的 API 十分类似。
应用这个 Monad,咱们能够重写后面的例子:
public Result<UserProfileResponse> getUserProfileHandler(final User.Id userId) {return resultUserService.findById(userId).flatMap(user -> resultUserProfileService.findById(userId)
.map(profile -> UserProfileResponse.of(user, profile)));
}
好吧,它与下面的示例基本相同,惟一的变动是 Monad — Result 而不是 Optional。与后面的例子不同,咱们有对于谬误的残缺信息,所以咱们能够在下层做一些事件。然而,只管残缺的错误处理代码依然很简略并且专一于业务逻辑。
“承诺是一个很重要的词。它要么成就了什么,要么毁坏了什么。”
我想展现的下一个 Monad 将是 Promise Monad。
必须抵赖,对于 Promise 是否是 monad,我还没有找到权威的答案。不同的作者对此有不同的认识。我纯正是从实用的角度来看它的:它的外观和行为与其余 monad 十分类似,所以我认为它们是一个 monad。
Promise Monad 代表一个(可能还不可用的)值。从某种意义上说,它与 Maybe Monad 十分类似。
Promise Monad 可用于示意譬如对外部服务或数据库的申请后果、文件读取或写入等。基本上它能够示意任何须要 I/O 和工夫来执行它的货色。Promise 反对与咱们在其余 Monad 中察看到的雷同的思维形式——“如果 / 当价值可用时做某事”。
请留神,因为无奈预测操作是否胜利,因而让 Promise 示意的不是 value 自身而是 Result 外部带有 value 是很不便的。
要理解它是如何工作的,让咱们看一下上面的示例:
...
public interface ArticleService {
// Returns list of articles for specified topics posted by specified users
Promise<Collection<Article>> userFeed(final Collection<Topic.Id> topics, final Collection<User.Id> users);
}
...
public interface TopicService {
// Returns list of topics created by user
Promise<Collection<Topic>> topicsByUser(final User.Id userId, final Order order);
}
...
public class UserTopicHandler {
private final ArticleService articleService;
private final TopicService topicService;
public UserTopicHandler(final ArticleService articleService, final TopicService topicService) {
this.articleService = articleService;
this.topicService = topicService;
}
public Promise<Collection<Article>> userTopicHandler(final User.Id userId) {return topicService.topicsByUser(userId, Order.ANY)
.flatMap(topicsList -> articleService.articlesByUserTopics(userId, topicsList.map(Topic::id)));
}
}
为了提供整个上下文,我蕴含了两个必要的接口,但实际上乏味的局部是 userTopicHandler() 办法。只管这种办法的简略性令人狐疑:
- 调用 TopicService 并检索由提供的用户创立的主题列表
- 胜利获取主题列表后,该办法提取主题 ID,而后调用 ArticleService,获取用户为指定主题创立的文章列表
-
执行端到端的错误处理
后记
Monads 是十分弱小和不便的工具。应用“当价值可用时做”的思维形式编写代码须要一些工夫来习惯,然而一旦你开始应用它,它将让你的生存变得更加简略。它容许将大量的心理开销卸载给编译器,并使许多谬误在编译时而不是在运行时变得不可能或可检测到。