使用Vavr进行功能性Java编程

许多人听说过Haskell和Clojure等功能语言。 但是例如,有类似Scala的语言。 它结合了OOP和功能方法。 好的旧Java呢? 是否可以在其上以功能样式编写程序,它可能会造成多少损失? 是的,有Java 8和带有流的lambda。 对于语言来说,这是迈出的一大步,但还远远不够。 在这种情况下可以提出一些建议吗? 原来是。



首先,让我们尝试确定以功能风格编写的代码意味着什么。 首先,我们必须不使用变量和对其进行操作,而要使用一些计算链。 本质上是一系列功能。 另外,我们必须具有特殊的数据结构。 例如,标准的Java集合不合适。 很快就会明白为什么。

让我们更详细地考虑功能结构。 任何此类结构必须至少满足两个条件:

  • 不可变 -结构必须是不可变的。 这意味着我们在创建阶段就将对象的状态固定下来,并保持其状态直至其存在。 一个明确的违反条件示例:标准ArrayList。
  • 持久的 -结构应尽可能长地存储在内存中。 如果我们创建了某个对象,则应该使用就绪对象,而不是创建具有相同状态的新对象。 更正式地讲,此类结构在修改后会保留其所有先前状态。 提及这些条件必须保持完全正常运行。

显然,我们需要某种第三方解决方案。 有一个解决方案: Vavr库。 如今,它是功能性样式中最受欢迎的Java库。 接下来,我将描述该库的主要功能。 许多示例,但绝非全部来自官方文档。

vavr库的主要数据结构


元组


元组是最基本,最简单的功能数据结构之一。 元组是固定长度的有序集合。 与列表不同,元组可以包含任何类型的数据。

Tuple tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42) 

获取所需的项来自调用元组中带有项号的字段。

 ((Tuple4) tuple)._1 // 1 

请注意:元组索引从1开始! 另外,要获得所需的元素,我们必须使用适当的方法集将对象转换为所需的类型。 在上面的示例中,我们使用了4个元素的元组,这意味着转换必须为Tuple4类型。 实际上,没有人阻止我们最初做出正确的类型。

 Tuple4 tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42) System.out.println(tuple._1); // 1 

前3个Vavr集合


清单


使用vavr创建列表非常简单。 比没有vavr更容易。

 List.of(1, 2, 3) 

这样的清单怎么办? 好吧,首先,我们可以将其转换为标准的Java列表。

 final boolean containThree = List.of(1, 2, 3) .asJava() .stream() .anyMatch(x -> x == 3); 

但是实际上,这没有什么必要,因为 我们可以这样做,例如:

 final boolean containThree = List.of(1, 2, 3) .find(x -> x == 1) .isDefined(); 

通常,标准的vavr列表具有许多有用的方法。 例如,有一个相当强大的卷积函数,可让您通过某些规则和中性元素组合值列表。

 //   final int zero = 0; //   final BiFunction<Integer, Integer, Integer> combine = (x, y) -> x + y; //   final int sum = List.of(1, 2, 3) .fold(zero, combine); //   

这里应注意一个重要点。 我们具有功能性的数据结构,这意味着我们无法更改其状态。 如何执行我们的清单? 数组不适合我们。

链接列表作为默认列表

让我们创建一个包含不可变对象的简单链接列表。 它看起来像这样:

图片

代码示例
 List list = List.of(1, 2, 3); 


列表中的每个元素都有两种主要方法:获取head元素(head)和所有其他元素(tail)。

代码示例
 list.head(); // 1 list.tail(); // List(2, 3) 


现在,如果要更改列表中的第一个元素(从1到0),则需要使用已完成零件的重用来创建新列表。

图片
代码示例
 final List tailList = list.tail(); //    tailList.prepend(0); //      


仅此而已! 由于工作表中的对象是不可变的,因此我们获得了线程安全和可重用的集合。 我们列表中的元素可以在应用程序中的任何位置应用,这是完全安全的!


另一个非常有用的数据结构是队列。 如何排成一列,以功能形式构建有效而可靠的程序? 例如,我们可以采用我们已经知道的数据结构:两个列表和一个元组。

图片

代码示例
 Queue<Integer> queue = Queue.of(1, 2, 3) .enqueue(4) .enqueue(5); 


当第一个结束时,我们将第二个展开并用于阅读。

图片

图片

重要的是要记住,队列必须像其他所有结构一样保持不变。 但是,不变的队列有什么用? 实际上,有一个窍门。 作为队列的可接受值,我们得到两个元素的元组。 首先:所需的队列元素,其次:没有此元素的队列发生了什么。

 System.out.println(queue); // Queue(1, 2, 3, 4, 5) Tuple2<Integer, Queue<Integer>> tuple2 = queue.dequeue(); System.out.println(tuple2._1); // 1 System.out.println(tuple2._2); // Queue(2, 3, 4, 5) 


下一个重要的数据结构是流。 流是对某些(通常是抽象的)值集执行某些操作的流。

有人可能会说Java 8已经拥有完善的流,而我们根本不需要新的流。 是这样吗

首先,让我们确保Java流不是功能数据结构。 检查结构的可变性。 为此,创建一个很小的流:
 IntStream standardStream = IntStream.range(1, 10); 

我们将对流中的所有元素进行排序:

 standardStream.forEach(System.out::print); 

作为响应,我们将输出输出到控制台: 123456789 。 让我们重复一下蛮力操作:

 standardStream.forEach(System.out::print); 

糟糕,发生以下错误:

 java.lang.IllegalStateException: stream has already been operated upon or closed 

事实是,标准流只是迭代器上的某种抽象。 尽管从外部看,这些流极其独立且强大,但是迭代器的缺点并没有消失。

例如,流的定义没有说明有关限制元素数量的任何内容。 不幸的是,它存在于迭代器中,这意味着它存在于标准流中。

幸运的是,vavr库解决了这些问题。 确保这一点:

 Stream stream = Stream.range(1, 10); stream.forEach(System.out::print); stream.forEach(System.out::print); 

作为响应,我们得到123456789123456789 。 这意味着第一个操作并未“破坏”我们的工作流。

让我们尝试创建一个无尽的流:

流infiniteStream = Stream.from(1);
System.out.println(infiniteStream); //流(1 ,?)

请注意:打印对象时,我们得到的不是无限结构,而是第一个元素和一个问号。 事实是流中的每个后续元素都是动态生成的。 这种方法称为延迟初始化。 是他使您可以安全地使用此类结构。

如果您从未使用过无限数据结构,那么您很可能在想:为什么这样做是必要的? 但是它们可以非常方便。 我们编写一个流,该流返回任意数量的奇数,将它们转换为字符串并添加一个空格:

 Stream oddNumbers = Stream .from(1, 2) //  1   2 .map(x -> x + " "); //  //   oddNumbers.take(5) .forEach(System.out::print); // 1 3 5 7 9 oddNumbers.take(10) .forEach(System.out::print); // 1 3 5 7 9 11 13 15 17 19 

好简单

馆藏的总体结构


在讨论了基本结构之后,该看一下vavr功能集合的一般体系结构了:



该结构的每个元素都可以迭代使用:

 StringBuilder builder = new StringBuilder(); for (String word : List.of("one", "two", "tree")) { if (builder.length() > 0) { builder.append(", "); } builder.append(word); } System.out.println(builder.toString()); // one, two, tree 

但是您应该三思而后行,然后再使用坞站。 该库使您可以简化熟悉的事情。

 System.out.println(List.of("one", "two", "tree").mkString(", ")); // one, two, tree 

使用功能


该库具有许多功能(共8个)和使用它们的有用方法。 它们是具有许多有趣方法的普通功能接口。 函数的名称取决于接受的参数数量(从0到8)。 例如, Function0不接受参数, Function1接受一个参数, Function2接受两个, 依此类推。

 Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; System.out.println(combineName.apply("Griffin", "Peter")); // Peter Griffin 

在vavr库的功能中,我们可以做很多很酷的事情。 在功能方面,它们远远超过了标准Function,BiFunction等。 例如,柯里化。 固化是部分功能的构建。 让我们看一个例子:

 //    Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; //           Function1<String, String> makeGriffinName = combineName .curried() .apply("Griffin"); //      System.out.println(makeGriffinName.apply("Peter")); // Peter Griffin System.out.println(makeGriffinName.apply("Lois")); // Lois Griffin 

如您所见,非常简洁。 咖喱的方法非常简单,但可能非常有用。

咖喱方法的实施
 @Override default Function1<T1, Function1<T2, R>> curried() { return t1 -> t2 -> apply(t1, t2); } 


函数集中有许多有用的方法。 例如,您可以缓存函数的返回结果:

 Function0<Double> hashCache = Function0.of(Math::random).memoized(); double randomValue1 = hashCache.apply(); double randomValue2 = hashCache.apply(); System.out.println(randomValue1 == randomValue2); // true 


与例外作斗争


如前所述,编程过程必须安全。 为此,有必要避免各种外来影响。 例外是它们的显式生成器。

您可以使用Try类以功能样式安全地处理异常。 实际上,这是典型的单子 。 不必研究该理论即可使用。 只要看一个简单的例子:

 Try.of(() -> 4 / 0) .onFailure(System.out::println) .onSuccess(System.out::println); 

从示例中可以看到,一切都很简单。 我们只是将事件挂在潜在错误上,而不是将其超出计算范围。

模式匹配


通常情况下,我们需要检查变量的值并根据结果对程序的行为进行建模。 正是在这种情况下,一个出色的模板搜索引擎才得以解决。 您不再需要编写一堆if else ,只需将所有逻辑都放在一个位置即可。

 import static io.vavr.API.*; import static io.vavr.Predicates.*; public class PatternMatchingDemo { public static void main(String[] args) { String s = Match(1993).of( Case($(42), () -> "one"), Case($(anyOf(isIn(1990, 1991, 1992), is(1993))), "two"), Case($(), "?") ); System.out.println(s); // two } } 

请注意,大小写大写,因为 case是一个关键字,已经被使用。

结论


我认为该库非常酷,但是值得非常谨慎地使用它。 她可以在事件驱动的开发中发挥出色。 但是,在基于线程池的标准命令式编程中过度而漫不经心地使用它会带来很多麻烦。 另外,通常在我们的项目中,我们使用Spring和Hibernate,但它们并不总是准备好用于这样的应用程序。 在将库导入项目之前,您需要清楚地了解如何以及为什么使用它。 我将在下一篇文章中谈论什么。

Source: https://habr.com/ru/post/zh-CN421839/


All Articles