Introduction to Vavr – Vavr简介

最后修改: 2016年 12月 29日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we are going to explore exactly what Vavr is, why we need it and how to use it in our projects.

在本文中,我们将探讨到底什么是Vavr,为什么我们需要它以及如何在我们的项目中使用它。

Vavr is a functional library for Java 8+ that provides immutable data types and functional control structures.

Vavr是一个用于Java 8+的功能库,提供不可变的数据类型和功能控制结构。

1.1. Maven Dependency

1.1.Maven的依赖性

In order to use Vavr, you need to add the dependency:

为了使用Vavr,你需要添加依赖关系。

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

It is recommended to always use the latest version. You can get it by following this link.

建议始终使用最新的版本。你可以按照这个链接获得它。

2. Option

2.选项

The main goal of Option is to eliminate null checks in our code by leveraging the Java type system.

Option的主要目标是通过利用Java类型系统来消除我们代码中的null检查。

Option is an object container in Vavr with a similar end goal like Optional in Java 8. Vavr’s Option implements Serializable, Iterable, and has a richer API.

Option是Vavr中的一个对象容器,其最终目标与Java 8中的Optional相似。Vavr的Option实现了Serializable,Iterable,并拥有更丰富的API

Since any object reference in Java can have a null value, we usually have to check for nullity with if statements before using it. These checks make the code robust and stable:

由于Java中的任何对象引用都可能有null值,所以我们通常要在使用前用if语句检查是否为空。这些检查使代码健壮而稳定。

@Test
public void givenValue_whenNullCheckNeeded_thenCorrect() {
    Object possibleNullObj = null;
    if (possibleNullObj == null) {
        possibleNullObj = "someDefaultValue";
    }
    assertNotNull(possibleNullObj);
}

Without checks, the application can crash due to a simple NPE:

如果没有检查,应用程序可能因简单的NPE:而崩溃。

@Test(expected = NullPointerException.class)
public void givenValue_whenNullCheckNeeded_thenCorrect2() {
    Object possibleNullObj = null;
    assertEquals("somevalue", possibleNullObj.toString());
}

However, the checks make the code verbose and not so readable, especially when the if statements end up being nested multiple times.

然而,这些检查使代码变得冗长且不那么可读,特别是当if语句最终被嵌套多次时。

Option solves this problem by totally eliminating nulls and replacing them with a valid object reference for each possible scenario.

Option通过完全消除nulls来解决这个问题,并在每种可能的情况下用一个有效的对象引用来取代它们。

With Option a null value will evaluate to an instance of None, while a non-null value will evaluate to an instance of Some:

通过Optionnull值将被评估为None的实例,而非null值将被评估为Some的实例。

@Test
public void givenValue_whenCreatesOption_thenCorrect() {
    Option<Object> noneOption = Option.of(null);
    Option<Object> someOption = Option.of("val");

    assertEquals("None", noneOption.toString());
    assertEquals("Some(val)", someOption.toString());
}

Therefore, instead of using object values directly, it’s advisable to wrap them inside an Option instance as shown above.

因此,最好不要直接使用对象的值,而是将它们包裹在一个Option实例中,如上图所示。

Notice, that we did not have to do a check before calling toString yet we did not have to deal with a NullPointerException as we had done before. Option’s toString returns us meaningful values in each call.

注意,我们在调用toString之前不必做检查,也不必像以前那样处理NullPointerException。Option的toString在每次调用时都会给我们返回有意义的值。

In the second snippet of this section, we needed a null check, in which we would assign a default value to the variable, before attempting to use it. Option can deal with this in a single line, even if there is a null:

在本节的第二个片段中,我们需要一个null检查,在尝试使用该变量之前,我们将为其分配一个默认值。Option可以在一行中处理这个问题,即使有一个null。

@Test
public void givenNull_whenCreatesOption_thenCorrect() {
    String name = null;
    Option<String> nameOption = Option.of(name);
   
    assertEquals("baeldung", nameOption.getOrElse("baeldung"));
}

Or a non-null:

或者一个非空的。

@Test
public void givenNonNull_whenCreatesOption_thenCorrect() {
    String name = "baeldung";
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("notbaeldung"));
}

Notice how, without null checks, we can get a value or return a default in a single line.

注意,在没有null检查的情况下,我们可以在一行中获得一个值或返回一个默认值。

3. Tuple

3.元组

There is no direct equivalent of a tuple data structure in Java. A tuple is a common concept in functional programming languages. Tuples are immutable and can hold multiple objects of different types in a type-safe manner.

在Java中没有直接等同于元组的数据结构。元组是函数式编程语言中的一个常见概念。元组是不可变的,并且可以以类型安全的方式容纳多个不同类型的对象。

Vavr brings tuples to Java 8. Tuples are of type Tuple1, Tuple2 to Tuple8 depending on the number of elements they are to take.

Vavr为Java 8带来了图元。图元的类型是Tuple1, Tuple2Tuple8,这取决于它们的元素数量。

There is currently an upper limit of eight elements. We access elements of a tuple like tuple._n where n is similar to the notion of an index in arrays:

目前有一个8个元素的上限。我们访问一个元组的元素,比如tuple._n,其中n类似于数组中的索引概念。

public void whenCreatesTuple_thenCorrect1() {
    Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
    String element1 = java8._1;
    int element2 = java8._2();

    assertEquals("Java", element1);
    assertEquals(8, element2);
}

Notice that the first element is retrieved with n==1. So a tuple does not use a zero base like an array. The types of the elements that will be stored in the tuple must be declared in its type declaration as shown above and below:

请注意,第一个元素是以n==1检索的。所以一个元组不像数组那样使用零基。将被存储在元组中的元素的类型必须在其类型声明中声明,如上面和下面所示。

@Test
public void whenCreatesTuple_thenCorrect2() {
    Tuple3<String, Integer, Double> java8 = Tuple.of("Java", 8, 1.8);
    String element1 = java8._1;
    int element2 = java8._2();
    double element3 = java8._3();
        
    assertEquals("Java", element1);
    assertEquals(8, element2);
    assertEquals(1.8, element3, 0.1);
}

A tuple’s place is in storing a fixed group of objects of any type that are better processed as a unit and can be passed around. A more obvious use case is returning more than one object from a function or a method in Java.

元组的作用是存储一组固定的任何类型的对象,这些对象最好作为一个单元来处理,并且可以被传递。一个更明显的用例是在Java中从一个函数或方法中返回一个以上的对象

4. Try

4.尝试

In Vavr, Try is a container for a computation which may result in an exception.

在Vavr中,Try是一个计算的容器 它可能导致一个异常。

As Option wraps a nullable object so that we don’t have to explicitly take care of nulls with if checks, Try wraps a computation so that we don’t have to explicitly take care of exceptions with try-catch blocks.

如同Option封装了一个nullable对象,这样我们就不必通过if检查来明确地处理nullsTry封装了一个计算,这样我们就不必通过try-catch块来明确地处理异常。

Take the following code for example:

以下面的代码为例。

@Test(expected = ArithmeticException.class)
public void givenBadCode_whenThrowsException_thenCorrect() {
    int i = 1 / 0;
}

Without try-catch blocks, the application would crash. In order to avoid this, you would need to wrap the statement in a try-catch block. With Vavr, we can wrap the same code in a Try instance and get a result:

如果没有try-catch块,应用程序将崩溃。为了避免这种情况,你需要将该语句包裹在一个try-catch块中。有了Vavr,我们可以把同样的代码包在一个Try实例中,并得到一个结果。

@Test
public void givenBadCode_whenTryHandles_thenCorrect() {
    Try<Integer> result = Try.of(() -> 1 / 0);

    assertTrue(result.isFailure());
}

Whether the computation was successful or not can then be inspected by choice at any point in the code.

然后,计算是否成功,可以在代码的任何一点选择检查。

In the above snippet, we have chosen to simply check for success or failure. We can also choose to return a default value:

在上面的片段中,我们选择了简单地检查成功或失败。我们也可以选择返回一个默认值。

@Test
public void givenBadCode_whenTryHandles_thenCorrect2() {
    Try<Integer> computation = Try.of(() -> 1 / 0);
    int errorSentinel = result.getOrElse(-1);

    assertEquals(-1, errorSentinel);
}

Or even to explicitly throw an exception of our choice:

甚至可以明确地抛出一个我们选择的异常。

@Test(expected = ArithmeticException.class)
public void givenBadCode_whenTryHandles_thenCorrect3() {
    Try<Integer> result = Try.of(() -> 1 / 0);
    result.getOrElseThrow(ArithmeticException::new);
}

In all the above cases, we have control over what happens after the computation, thanks to Vavr’s Try.

在上述所有情况下,由于Vavr的Try,我们可以控制计算之后的情况。

5. Functional Interfaces

5.功能性接口

With the arrival of Java 8, functional interfaces are inbuilt and easier to use, especially when combined with lambdas.

随着Java 8的到来,functional interfaces被内置并更容易使用,特别是与lambdas结合使用时。

However, Java 8 provides only two basic functions. One takes only a single parameter and produces a result:

然而,Java 8只提供了两个基本函数。一个只接受一个参数并产生一个结果。

@Test
public void givenJava8Function_whenWorks_thenCorrect() {
    Function<Integer, Integer> square = (num) -> num * num;
    int result = square.apply(2);

    assertEquals(4, result);
}

The second only takes two parameters and produces a result:

第二种只需要两个参数并产生一个结果。

@Test
public void givenJava8BiFunction_whenWorks_thenCorrect() {
    BiFunction<Integer, Integer, Integer> sum = 
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

On the flip side, Vavr extends the idea of functional interfaces in Java further by supporting up to a maximum of eight parameters and spicing up the API with methods for memoization, composition, and currying.

另一方面,Vavr进一步扩展了Java中功能接口的概念,它最多支持8个参数,并通过记忆化、组合和凝结的方法对API进行调剂。

Just like tuples, these functional interfaces are named according to the number of parameters they take: Function0, Function1, Function2 etc. With Vavr, we would have written the above two functions like this:

就像图元一样,这些函数接口是根据它们的参数数量来命名的。Function0, Function1, Function2等等。在Vavr中,我们会把上述两个函数写成这样。

@Test
public void givenVavrFunction_whenWorks_thenCorrect() {
    Function1<Integer, Integer> square = (num) -> num * num;
    int result = square.apply(2);

    assertEquals(4, result);
}

and this:

和这个。

@Test
public void givenVavrBiFunction_whenWorks_thenCorrect() {
    Function2<Integer, Integer, Integer> sum = 
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

When there is no parameter but we still need an output, in Java 8 we would need to use a Supplier type, in Vavr Function0 is there to help:

当没有参数但我们仍然需要输出时,在Java 8中,我们需要使用Supplier 类型,在Vavr中,Function0是用来帮助的。

@Test
public void whenCreatesFunction_thenCorrect0() {
    Function0<String> getClazzName = () -> this.getClass().getName();
    String clazzName = getClazzName.apply();

    assertEquals("com.baeldung.vavr.VavrTest", clazzName);
}

How about a five parameter function, it’s just a matter of using Function5:

五个参数的函数如何,这只是一个使用Function5的问题。

@Test
public void whenCreatesFunction_thenCorrect5() {
    Function5<String, String, String, String, String, String> concat = 
      (a, b, c, d, e) -> a + b + c + d + e;
    String finalString = concat.apply(
      "Hello ", "world", "! ", "Learn ", "Vavr");

    assertEquals("Hello world! Learn Vavr", finalString);
}

We can also combine the static factory method FunctionN.of for any of the functions to create a Vavr function from a method reference. Like if we have the following sum method:

我们还可以结合任何一个函数的静态工厂方法FunctionN.of,从一个方法引用中创建一个Vavr函数。比如我们有以下sum方法。

public int sum(int a, int b) {
    return a + b;
}

We can create a function out of it like this:

我们可以像这样创建一个函数出来。

@Test
public void whenCreatesFunctionFromMethodRef_thenCorrect() {
    Function2<Integer, Integer, Integer> sum = Function2.of(this::sum);
    int summed = sum.apply(5, 6);

    assertEquals(11, summed);
}

6. Collections

6.收藏

The Vavr team has put a lot of effort in designing a new collections API that meets the requirements of functional programming i.e. persistence, immutability.

Vavr团队在设计一个新的集合API方面付出了大量的努力,它满足了功能化编程的要求,即持久性、不变性。

Java collections are mutable, making them a great source of program failure, especially in the presence of concurrency. The Collection interface provides methods such as this:

Java集合是可变的,这使得它们成为程序失败的一大根源,尤其是在有并发症的情况下。Collection接口提供了这样的方法。

interface Collection<E> {
    void clear();
}

This method removes all elements in a collection(producing a side-effect) and returns nothing. Classes such as ConcurrentHashMap were created to deal with the already created problems.

这个方法删除了一个集合中的所有元素(产生副作用),并且不返回任何东西。诸如ConcurrentHashMap这样的类被创建来处理已经产生的问题。

Such a class does not only add zero marginal benefits but also degrades the performance of the class whose loopholes it is trying to fill.

这样的阶层不仅没有增加边际效益,而且还降低了其试图填补的漏洞的阶层的表现。

With immutability, we get thread-safety for free: no need to write new classes to deal with a problem that should not be there in the first place.

有了不变性,我们就可以免费获得线程安全:不需要编写新的类来处理一个本来就不应该存在的问题。

Other existing tactics to add immutability to collections in Java still create more problems, namely, exceptions:

其他现有的在Java中为集合添加不变性的策略仍然会产生更多的问题,即异常。

@Test(expected = UnsupportedOperationException.class)
public void whenImmutableCollectionThrows_thenCorrect() {
    java.util.List<String> wordList = Arrays.asList("abracadabra");
    java.util.List<String> list = Collections.unmodifiableList(wordList);
    list.add("boom");
}

All the above problems are non-existent in Vavr collections.

上述所有问题在Vavr收藏中都不存在。

To create a list in Vavr:

要在Vavr中创建一个列表。

@Test
public void whenCreatesVavrList_thenCorrect() {
    List<Integer> intList = List.of(1, 2, 3);

    assertEquals(3, intList.length());
    assertEquals(new Integer(1), intList.get(0));
    assertEquals(new Integer(2), intList.get(1));
    assertEquals(new Integer(3), intList.get(2));
}

APIs are also available to perform computations on the list in place:

API也可以用来对列表进行就地计算。

@Test
public void whenSumsVavrList_thenCorrect() {
    int sum = List.of(1, 2, 3).sum().intValue();

    assertEquals(6, sum);
}

Vavr collections offer most of the common classes found in the Java Collections Framework and actually, all features are implemented.

Vavr集合提供了大部分在Java集合框架中发现的普通类,实际上,所有的功能都已经实现。

The takeaway is immutability, removal of void return types and side-effect producing APIs, a richer set of functions to operate on the underlying elements, very short, robust and compact code compared to Java’s collection operations.

我们的收获是:不变性去除无效返回类型产生副作用的API,一套更丰富的函数来操作底层元素,与Java的集合操作相比,代码非常简短、稳健和紧凑。

A full coverage of Vavr collections is beyond the scope of this article.

对Vavr收藏的全面覆盖超出了本文的范围。

7. Validation

7.验证

Vavr brings the concept of Applicative Functor to Java from the functional programming world. In the simplest of terms, an Applicative Functor enables us to perform a sequence of actions while accumulating the results.

Vavr将应用性向量的概念从函数式编程世界带到了Java。用最简单的术语来说,一个应用性向量使我们能够执行一连串的动作,同时积累结果

The class vavr.control.Validation facilitates the accumulation of errors. Remember that, usually, a program terminates as soon as an error is encountered.

vavr.control.Validation类便于积累错误。记住,通常情况下,一旦遇到错误,程序就会终止。

However, Validation continues processing and accumulating the errors for the program to act on them as a batch.

然而,Validation继续处理并积累错误,以便程序作为一个批次对它们采取行动。

Consider that we are registering users by name and age and we want to take all input first and decide whether to create a Person instance or return a list of errors. Here is our Person class:

考虑到我们正在按姓名年龄注册用户,我们想先接受所有的输入,并决定是创建一个Person实例还是返回一个错误列表。这里是我们的Person类。

public class Person {
    private String name;
    private int age;

    // standard constructors, setters and getters, toString
}

Next, we create a class called PersonValidator. Each field will be validated by one method and another method can be used to combine all the results into one Validation instance:

接下来,我们创建一个名为PersonValidator的类。每个字段将由一个方法来验证,另一个方法可以用来将所有的结果合并成一个Validation实例。

class PersonValidator {
    String NAME_ERR = "Invalid characters in name: ";
    String AGE_ERR = "Age must be at least 0";

    public Validation<Seq<String>, Person> validatePerson(
      String name, int age) {
        return Validation.combine(
          validateName(name), validateAge(age)).ap(Person::new);
    }

    private Validation<String, String> validateName(String name) {
        String invalidChars = name.replaceAll("[a-zA-Z ]", "");
        return invalidChars.isEmpty() ? 
          Validation.valid(name) 
            : Validation.invalid(NAME_ERR + invalidChars);
    }

    private Validation<String, Integer> validateAge(int age) {
        return age < 0 ? Validation.invalid(AGE_ERR)
          : Validation.valid(age);
    }
}

The rule for age is that it should be an integer greater than 0 and the rule for name is that it should contain no special characters:

年龄的规则是,它应该是一个大于0的整数,名字的规则是,它不应该包含特殊字符。

@Test
public void whenValidationWorks_thenCorrect() {
    PersonValidator personValidator = new PersonValidator();

    Validation<List<String>, Person> valid = 
      personValidator.validatePerson("John Doe", 30);

    Validation<List<String>, Person> invalid = 
      personValidator.validatePerson("John? Doe!4", -1);

    assertEquals(
      "Valid(Person [name=John Doe, age=30])", 
        valid.toString());

    assertEquals(
      "Invalid(List(Invalid characters in name: ?!4, 
        Age must be at least 0))", 
          invalid.toString());
}

A valid value is contained in a Validation.Valid instance, a list of validation errors is contained in a Validation.Invalid instance. So any validation method must return one of the two.

一个有效的值包含在Validation.Valid实例中,一个验证错误列表包含在Validation.Invalid实例中。所以任何验证方法都必须返回这两者中的一个。

Inside Validation.Valid is an instance of Person while inside Validation.Invalid is a list of errors.

Validation.Valid中是一个Person的实例,而在Validation.Invalid中是一个错误列表。

8. Lazy

8.懒惰

Lazy is a container which represents a value computed lazily i.e. computation is deferred until the result is required. Furthermore, the evaluated value is cached or memoized and returned again and again each time it is needed without repeating the computation:

Lazy是一个容器,它表示一个懒惰地计算的值,也就是说,计算被推迟到需要结果的时候。此外,被评估的值被缓存或备忘,并在每次需要时反复返回,而不重复计算。

@Test
public void givenFunction_whenEvaluatesWithLazy_thenCorrect() {
    Lazy<Double> lazy = Lazy.of(Math::random);
    assertFalse(lazy.isEvaluated());
        
    double val1 = lazy.get();
    assertTrue(lazy.isEvaluated());
        
    double val2 = lazy.get();
    assertEquals(val1, val2, 0.1);
}

In the above example, the function we are evaluating is Math.random. Notice that, in the second line, we check the value and realize that the function has not yet been executed. This is because we still haven’t shown interest in the return value.

在上面的例子中,我们正在评估的函数是Math.random。请注意,在第二行中,我们检查了数值,发现该函数还没有被执行。这是因为我们仍然没有对返回值表现出兴趣。

In the third line of code, we show interest in the computation value by calling Lazy.get. At this point, the function executes and Lazy.evaluated returns true.

在第三行代码中,我们通过调用Lazy.get表明对计算值的兴趣。这时,函数执行,Lazy.evaluated返回true。

We also go ahead and confirm the memoization bit of Lazy by attempting to get the value again. If the function we provided was executed again, we would definitely receive a different random number.

我们还通过再次尝试get值来确认Lazy的记忆化位。如果我们提供的函数被再次执行,我们肯定会收到一个不同的随机数。

However, Lazy again lazily returns the initially computed value as the final assertion confirms.

然而,Lazy再次懒散地返回最初的计算值,正如最后的断言所确认的那样。

9. Pattern Matching

9.模式匹配

Pattern matching is a native concept in almost all functional programming languages. There is no such thing in Java for now.

模式匹配是几乎所有函数式编程语言中的一个原生概念。在Java中暂时还没有这样的东西。

Instead, whenever we want to perform a computation or return a value based on the input we receive, we use multiple if statements to resolve the right code to execute:

相反,每当我们想根据收到的输入进行计算或返回一个值时,我们使用多个if语句来解决要执行的正确代码。

@Test
public void whenIfWorksAsMatcher_thenCorrect() {
    int input = 3;
    String output;
    if (input == 0) {
        output = "zero";
    }
    if (input == 1) {
        output = "one";
    }
    if (input == 2) {
        output = "two";
    }
    if (input == 3) {
        output = "three";
    }
    else {
        output = "unknown";
    }

    assertEquals("three", output);
}

We can suddenly see the code spanning multiple lines while just checking three cases. Each check is taking up three lines of code. What if we had to check up to a hundred cases, those would be about 300 lines, not nice!

我们可以突然看到代码跨越了多行,而只是检查三种情况。每个检查都占用了三行代码。如果我们要检查多达一百个案例,那将是大约300行的代码,这可不是什么好事啊

Another alternative is using a switch statement:

另一个选择是使用switch语句。

@Test
public void whenSwitchWorksAsMatcher_thenCorrect() {
    int input = 2;
    String output;
    switch (input) {
    case 0:
        output = "zero";
        break;
    case 1:
        output = "one";
        break;
    case 2:
        output = "two";
        break;
    case 3:
        output = "three";
        break;
    default:
        output = "unknown";
        break;
    }

    assertEquals("two", output);
}

Not any better. We are still averaging 3 lines per check. A lot of confusion and potential for bugs. Forgetting a break clause is not an issue at compile time but can result in hard-to-detect bugs later on.

没有任何改善。我们仍然是平均每次检查3行。大量的混乱和潜在的bug。忘记一个break子句在编译时不是问题,但在以后会导致难以检测的bug。

In Vavr, we replace the entire switch block with a Match method. Each case or if statement is replaced by a Case method invocation.

在Vavr中,我们将整个switch块替换为Match方法。每个caseif语句被一个Case方法调用所取代。

Finally, atomic patterns like $() replace the condition which then evaluates an expression or value. We also provide this as the second parameter to Case:

最后,像$()这样的原子模式取代了条件,然后评估了一个表达式或值。我们还将其作为第二个参数提供给Case

@Test
public void whenMatchworks_thenCorrect() {
    int input = 2;
    String output = Match(input).of(
      Case($(1), "one"), 
      Case($(2), "two"), 
      Case($(3), "three"),
      Case($(), "?"));
 
    assertEquals("two", output);
}

Notice how compact the code is, averaging only one line per check. The pattern matching API is way more powerful than this and can do more complex stuff.

注意到代码是多么紧凑,平均每个检查只有一行。模式匹配API比这更强大,可以做更复杂的事情。

For example, we can replace the atomic expressions with a predicate. Imagine we are parsing a console command for help and version flags:

例如,我们可以用一个谓词来代替原子表达式。想象一下,我们正在解析一个控制台命令的helpversion标志。

Match(arg).of(
    Case($(isIn("-h", "--help")), o -> run(this::displayHelp)),
    Case($(isIn("-v", "--version")), o -> run(this::displayVersion)),
    Case($(), o -> run(() -> {
        throw new IllegalArgumentException(arg);
    }))
);

Some users may be more familiar with the shorthand version (-v) while others, with the full version (–version). A good designer must consider all these cases.

一些用户可能更熟悉速记版本(-v),而另一些用户则熟悉完整版本(-version)。一个好的设计师必须考虑所有这些情况。

Without the need for several if statements, we have taken care of multiple conditions. We will learn more about predicates, multiple conditions, and side-effects in pattern matching in a separate article.

在不需要几个if语句的情况下,我们已经照顾到了多个条件。我们将在另一篇文章中学习更多关于谓词、多条件和模式匹配的副作用。

10. Conclusion

10.结论

In this article, we have introduced Vavr, the popular functional programming library for Java 8. We have tackled the major features that we can quickly adapt to improve our code.

在这篇文章中,我们介绍了Vavr,这个流行的Java 8函数式编程库。我们已经解决了一些主要的功能,我们可以快速适应这些功能来改进我们的代码。

The full source code for this article is available in the Github project.

本文的完整源代码可在Github项目中找到。