关于java:Java-设计模式-Monads-的美丽世界

7次阅读

共计 5643 个字符,预计需要花费 15 分钟才能阅读完成。

【注】本文译自:[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 是十分弱小和不便的工具。应用“当价值可用时做”的思维形式编写代码须要一些工夫来习惯,然而一旦你开始应用它,它将让你的生存变得更加简略。它容许将大量的心理开销卸载给编译器,并使许多谬误在编译时而不是在运行时变得不可能或可检测到。

正文完
 0