什么是Monad?Java 开发人员的基本理论
monad 是一个概念,源自数学的一部分,称为 范畴论,而不是类或特征。在本文中,我将尝试解释其结构和内部工作原理。通过使用Java 中的Optional ,我将尝试以更易于理解的方式描述所有这些。我还将实现一个基本的 monad 以更好地理解它们的工作原理,并以一个简短的使用示例结束,以展示 monad 相对于非 monad 方法的优势。
为什么要学习 Monads 的工作原理?
首先,对我们使用的东西如何工作有一个基本的了解总是好的。如果您是 Java 开发人员,您可能会使用 monad,甚至可能不知道。您可能会感到惊讶,但两个最著名的Java 8 特性,即Stream和Optional是 monad 实现。
另外,现在函数式编程越来越流行,所以我们可能会有更多类似的单子结构。在这种情况下,了解 monad 是什么以及它是如何工作的可能会变得更加有价值。
让我们从描述 monad 是什么开始——或多或少准确。在我看来,这里的问题相当简单。
什么是Monad?
阅读介绍后,您就会知道 monad 是范畴论中的一个概念。在软件世界中,它可以在任何具有泛型支持的静态类型语言中实现为类或特征。此外,我们可以将它视为一个包装器,它将我们的值放在某个上下文中,并允许我们对值执行操作,特别是返回包装在同一上下文中的值的操作。此外,我们可以以这样的方式链接操作,即任何步骤的操作输出都是下一步操作的输入。
现代编程语言中的 monad 示例:
流(Java)。
可选/选项(Java/Scala)。
要么(斯卡拉)。
试试(斯卡拉)。
IO Monad (Haskell)。
Monad定律
在谈到 monad 时,最后需要提及的是它们的定律。如果我们想将我们的实现视为真正的 monad,我们必须遵守它们。存在三个定律:左恒等式、右恒等式和结合律。在我看来,理解它们的实际含义可能有点困难。
现在,在 Optional的帮助下,我将尝试更详细地解释上述规律。
但首先有几个假设:
f 是从类型 T 到类型 Optional<R> 的函数映射
g 是从类型 R 到类型 Optional<U> 的函数映射
1、Left identity
如果我们创建一个新的 monad 并将其绑定到函数,结果应该与将函数应用于值相同。
Optional<String> leftIdentity = 域名(x).flatMap(f); Optional<String> mappedX = 域名y(x); assert 域名ls(mappedX);
2、Right identity
将单元函数绑定到 monad 的结果应该与创建新 monad 的结果相同。
Optional<Integer> rightIdentity = 域名(x).flatMap(Optional::of); Optional<Integer> wrappedX = 域名(x); assert 域名ls(wrappedX);
3、Associativity
在函数应用链中,函数如何嵌套应该无关紧要。
Optional<Long> leftSide = 域名(x).flatMap(f).flatMap(g); Optional<Long> rightSide = 域名(x).flatMap(v -> 域名y(v).flatMap(g)); assert 域名ls(rightSide);
Monad的创建
现在,当我们了解了基础知识后,我们就可以专注于实施了。
我们需要的第一件事是参数化类型 M<T>,它是我们类型 T 值的包装器。我们的类型必须实现两个函数:
of ( unit ) 用于包装我们的值并具有以下签名M<T>(T)。
flatMap ( bind ) 负责执行操作。在这里,我们传递了一个函数,该函数对我们的上下文中的值进行操作,并使用已经包装在上下文中的另一种类型返回它。此方法应具有以下签名M<U> (T -> M<U>)。
为了让它更容易理解,我将再使用一次Optional并展示在这种情况下上述结构的样子。
在这里,第一个条件立即得到满足,因为Optional是一个参数化类型。该单元功能的作用是满足ofNullable和的方法。FlatMap 扮演绑定函数的角色。当然,在Optional的情况下,类型边界允许我们使用比上面定义更复杂的类型。
说完了理论,让我们实施
import 域名域名tion; public final class WrapperMonad<T> { private final T value; private WrapperMonad(T value) { 域名e = value; } static <T> WrapperMonad<T> of(T value) { return new WrapperMonad<>(value); } <U> WrapperMonad<U> flatMap(Function<T, WrapperMonad<U>> function) { return 域名y(value); } // For sake of asserting in Example boolean valueEquals(T x) { return 域名ls(x); } }
monad 实现了。让我们详细描述一下我在这里做了什么。
这里究竟发生了什么
我们实现的基础是带有名为“value”的不可变字段的参数化类,它负责存储我们的值。然后,我们有一个私有构造函数,这使得除了通过我们的包装方法 - of之外的任何其他方式创建对象都是不可能的。
接下来,我们有两个基本的单子功能,即中(相当于单位)和flatMap(相当于绑定),这将保证我们的执行符合所要求的条件的单子法律形式。
有了所描述的功能,现在是使用示例的时候了。所以在这里。
import 域名域名tion; public class Example { public static void main(String[] args) { int x = 2; // Task: performing operation, returning wrapped value, over the value inside the container object. // Non-Monad Function<Integer, Wrapper<String>> toString = i -> new Wrapper<>(域名ring()); Function<String, Wrapper<Integer>> hashCode = str -> new Wrapper<>(域名Code()); Wrapper<Integer> wrapper = new Wrapper<>(x); Wrapper<String> stringifyWrapper = 域名y(域名e); // One liner - Wrapper<Integer> hashCodedWrapper = 域名y(域名y(x).value); Wrapper<Integer> hashCodedWrapper = 域名y(域名e); // Monad Function<Integer, WrapperMonad<String>> toStringM = i -> 域名(域名ring()); Function<String, WrapperMonad<Integer>> hashCodeM = str -> 域名(域名Code()); WrapperMonad<Integer> hashCodedWrapperMonadic = 域名(x) .flatMap(toStringM) .flatMap(hashCodeM); assert 域名eEquals(域名e); 域名tln("Values inside wrappers are equal"); } }
在上面的代码中,除了看到 monad 是如何工作的,我们还可以看到使用它们的一些优点。
在 monadic 部分,所有操作都组合成一个单一的执行管道,这使得代码更具声明性,更易于阅读和理解。此外,如果我们决定有一天能加入错误处理逻辑,它可以很好地封装内部的和flatMap方法。
另一方面,在示例的非 monadic 部分,我们有一个不同的包私有字段值的设计,我们需要一种从包装器外部访问值的方法,这打破了封装。
总结
Monad是一个非常有用和强大的概念,可能我们很多人在日常工作中都会使用它。我试图对其背后的理论基础和逻辑进行清晰的描述性解释。我实现了一个自定义 monad 来表明它不是一个复杂的结构。在上面的例子中,我展示了 monad 的用法,这种方法的潜在优点是什么,以及它与普通方法调用的区别。