Java Monad 设计模式

简介

如果您以前从未使用过Monad,那么您几乎肯定会听说过它们。 Monad在纯函数式编程语言中被广泛使用,但是即使非函数式编程语言也使用Monad。 在本文中,我们将尝试将monad理解为一种设计模式,首先,将monad引入功能编程范例中,然后在Java特定上下文中对其进行探索。

什么是Monad?

Monad的概念最初来自称为范畴论的数学领域。 它最初是在1990年代初引入理论计算机科学的,以发展计算的分类语义,尤其是证明程序的等效性。 从那以后,单子在编程语言理论中的应用变得越来越复杂。 Monad在纯函数式编程语言中广泛使用,以集成命令式编程语言提供的某些灵活性。 结果,它们在编程社区中变得如此流行,以至于程序员将monads定义为设计模式。 一些不一定是功能性编程语言的编程语言广泛使用了monad。 其中最流行的是Java,它甚至具有可以充当monad的内置类。

Monad用在函数式编程

Monad可用于函数式编程中,以允许纯函数“具有副作用”。 第一个实现应用于Haskell,以便处理由于延迟评估而导致的I / O问题。 在Haskell中,所有函数都是纯函数,并且所有值都是惰性计算的。 但是,对于I / O操作,延迟评估并不是真正需要的。 为了获得成功的I / O,一些主要是函数式编程语言(如Lisp和SML)牺牲了函数的纯度。 相比之下,Haskell的I / O系统通过称为IO Monad的功能来保持功能纯净,这是功能编程中monad的众多用途之一。

将monad设计模式集成到代码中的一种方法是使用修饰的类型包装不纯净的代码或混淆(slippery)的代码。 在这里,如果代码的输入有些不正常,则将混淆代码视为容易出错的东西。 用修饰类型包装代码块对于抽象很有帮助,但这些代码块本身并没有真正的用处。 为此,我们还需要一些工具来将它们组合在一起,并相互连接。 以下示例对于理解此类修饰类型和Monadic工具的潜在用法将非常有帮助。

public Integer divide(Integer a, Integer b) {
    return a/b;
}
public Integer addTen(Integer a) {
    return a+10;
}

函数式编程的主要元素之一是具有可组合的方法,因此人们可以将一种方法的值直接输入另一种方法。 函数组合来自数学,并且在编程中也很常见。 下图很好地比较了数学和程序设计中的组成。

private String getCapitalName(String country){}
private Integer getPopulationOfCity(String city){}
getPopulationOfCity(getCapitalName("US")) // 组合

在这里,在我们的示例中,我们的两种方法也可以组成。 换句话说,我们可以将除法的值输入到addTen中(例如`addTen(divide(8,2))= addTen(4)= 14)。 可以猜测,当分母或除法的第二个参数为零时,这种组合可能会出错。 我们现在应该做什么?

首先确保方法是纯函数。 显而易见,方法add已经是一个纯函数,而除法不是。 因此,我们需要修改分度。

public Optional<Integer> divide(Integer a, Integer b) {
   if (b == 0) {
        return Optional.empty();
   }
   return Optional.of(a/b);
}

另一个更优雅的写法:

public Optional<Integer> divide(Integer a, Integer b) {
    return (b == 0) ? Optional.empty() : Optional.of(a/b);
}

在这一点上,我们可能会问为什么要使用Optionals而不使用null。 这是这篇文章尝试解释的问题。 只是为了给自己一个小小的破坏,事后看来,Option可能是明智之选,但我们不必知道,因为我们仍然不知道什么是单子。 坐好,我们到了。

由于我们的方法之一返回Optional,我们也可以将addTen更改为return Optional。 结果,我们将拥有:

public Optional<Integer> addTen(Integer a) {
    return Optional.of(a + 10);
}

正如我们所看到的,这些方法不再可以与我们通常的函数组合操作直接组合。 但是,我们仍然可以执行以下操作:

public Optional<Integer> divideAndAddTen(Integer a, Integer b) {
    final Optional<Integer> divisionResult = divide(a, b);
    if (divisionResult.isPresent()){
        return addTen(divisionResult.get());
    }
    return Optional.empty();
}

此方法将正常运行,并返回我们需要的结果。 但是,这仍然不是我们要寻找的“特殊功能组合”。 此方法不使用Monads。

Monad到底是什么? 已经讨论了很久了,但是还没有给出任何适当的定义。 好吧,推迟这么长时间有充分的理由。 在知道什么是Monad之前,我们需要了解为什么需要它。 现在,我们有一个用于monad的用例,因此让我们继续进行不太严格的定义:

定义:Monad是一个泛型类型构造函数(即JavaType-> JavaType)以及两个映射

  • unit 映射: 在给定类型构造函数的情况下,这适用于Java Type或函数。
  • flattener: 解析Java Type, 判断一个类型构造函数是否已经被用过至少两次

好的,这太抽象了。举几个例子,使这个定义更有意义。 假设我们有类型构造函数可选:: JavaType-> JavaType。 那么我们的单位映射将是:

unit:: T -> Optional<T>
unit:: (X -> Y) -> (Optional<X> -> Optional<Y>)

在Java Optional中, unit:: T -> Optional<T> 是 Optional.of()。然而unit:: (X -> Y) -> (Optional<X> -> Optional<Y>) 有点麻烦。在这种情况下, unit接收一个函数作为输入并返回它的执行格式。比如,假设parseInt::String->Integer 是个函数,接收String返回Integer。如果我们把unit应用到parseInt上,就会是:

unit(parseInt) :: Optional<String> -> Optional<Integer>

还有flattener

flattener:: Optional<Optional<T>> -> Optional<T>

在Java Optional中,就类似,但是不完全等于flatMap.

在这里,Optional是monad的定义,可以在monad的设计模式中使用。 Java中还有许多其他通用类型构造函数,例如List,Stream,Collection等,它们也可以在monad设计模式中使用。

现在,让我们回到原始示例。 由于我们已经使用了Optionals,因此让我们更进一步,以monadic形式重写我们的divideAndAddTen方法。 那将是:

Optional<Integer> divideAndAddTenWithMonad(Integer a, Integer b) {
    return divide(a,b)
          .flatMap(divisionResult -> addTen(divisionResult));
}

同样,由于此代码片段很简单,因此FlatMap与我们的monadic类型构造函数附带的flatten函数并不相同。 实际上,flatten是执行的最后一步。

现在,如果输入正确,此dividAndAddTenWithMonad将返回Optional <Integer>结果,否则将返回Optional.empty()。 因此,现在我们可以自信地说我们具有前面提到的“特殊功能组合”。 在函数式编程世界中,这表示为>=>。 因此,divideAndAddTenWithMonad基本上是addTen >=> divide

结论

Monad使我们能够在命令式程序中使用纯函数,同时处理所有副作用。 这听起来令人兴奋。 如何将纯功能和副作用放在一起?

Last modification:October 10, 2023
如果觉得我的文章对你有用,请随意赞赏