Vavr简介

1,249 阅读9分钟

什么是 Vavr?

Vavr 是一个函数式编程库,支持 Java 8+。它的第一个版本发布于2014年3月9号,叫 Javaslang

它主要由三部分组成

  1. Immutable collections,用来避免出现side-effect
  2. ADT(Algebraic data type),用来消除已存在的side-effect
  3. Pattern matching,用来更方便的使用ADT

我创建了一个repo叫 vavr-examples, 欢迎下载试玩。

如何安装 Vavr?

  • Maven

    <dependencies>
        <dependency>
            <groupId>io.vavr</groupId>
            <artifactId>vavr</artifactId>
            <version>0.10.4</version>
        </dependency>
    </dependencies>
    
  • Gradle

    dependencies {
        compile "io.vavr:vavr:0.10.4"
    }
    
  • Gradel 7 +

    dependencies {
        implementation "io.vavr:vavr:0.10.4"
    }
    

如何使用 Vavr?

函数式编程 是指用纯函数来构建程序。

纯函数需要满足下面的条件

  • 对于所有的输入,相同输入有相同的输出
  • 没有Side-Effect

例如下面的例子就不满足第一个条件

public Integer generateRandomNumber() {
    Random random = new Random();
    return random.nextInt();
}

@Test
public void returnDifferentResultForEachCall() {
    assertThat(generateRandomNumber()).isNotEqualTo(generateRandomNumber());
}

如果我们调用 generateRandomNumber 多次,它每次都会返回不同的 Integer。我们无法避免这个逻辑,无论如何我们都需要一个随机数。但是我们可以把生成随机数的逻辑挪到程序的边界处,这样能够确保大部分函数是纯函数。例如我们可以传入另外一个函数来生成随机数,或者先在main方法里生成随机数然后传给业务逻辑。

public Integer generateRandomNumber(Supplier<Integer> generator) {
    return generator.get();
}

@Test
public void returnSameResultForEachCall() {
    assertThat(generateRandomNumber(() -> 1)).isEqualTo(generateRandomNumber(() -> 1));
}

在下面的章节里,我们将主要讨论如何避免和消除side-effect。

避免 Side-Effect

下面的 append 是纯函数么?

public void append(List<Integer> intList1, List<Integer> intList2) {
    intList1.addAll(intList2);
    return;
}

@Test
public void modifyParameterIsSideEffect() {

    List<Integer> intList1 = new LinkedList<Integer>();
    intList1.add(1);
    intList1.add(2);

    List<Integer> intList2 = new LinkedList<Integer>();
    intList2.add(3);
    intList2.add(4);

    this.append(intList1, intList2);

    assertThat(intList1.size()).isEqualTo(4);
    assertThat(intList2.size()).isEqualTo(2);
}

答案是否定的。这个函数虽然返回了void,但是却修改了外部的intList1

我们可以把代码写成这样,那是因为List是可变的。为了避免对函数外部变量的修改,我需要使用不可变的数据类型。

Vavr对Java中的集合类型进行了重新定义,新的类型是不可变的。

List

我们可以使用下面的函数来构造List

java.util.List<Integer> javaList = new java.util.LinkedList<Integer>();
javaList.add(1);
javaList.add(2);
javaList.add(3);

List.of(1,2,3);
List.ofAll(javaList);

这个List是不可变的,对它的任何修改都会产生一个新的List实例。

public List<Integer> append(List<Integer> intList1, List<Integer> intList2) {
    return intList1.appendAll(intList2);
}

@Test
public void modifyParameterWillGenerateNewList() {

    List<Integer> intList1 = List.of(1, 2);

    List<Integer> intList2 = List.of(1, 2);

    List<Integer> result = this.append(intList1, intList2);

    assertThat(result.size()).isEqualTo(4);
    assertThat(intList1.size()).isEqualTo(2);
    assertThat(intList2.size()).isEqualTo(2);
}

我们还可以直接对List中的元素进行比较

@Test
public void listCanCompareElement() {
    assertThat(List.of(1, 2)).isEqualTo(List.of(1, 2));
    assertThat(List.of(1, 2)).isNotEqualTo(List.of(1, 2, 3));
}

消除 Side-Effect

Option

在Java中,函数返回null是很常见的,例如

public Integer getHead(java.util.List<Integer> intList){
  if(intList.size() == 0 ){
     return null;
  } else {
     return intList.get(0);
  }
}

@Test
public void consumerNeedToCheckNull() {
    Integer head = getHead(new LinkedList<Integer>());

    String result = head == null ? "No Element" : head.toString();

    assertThat(result).isEqualTo("No Element");
}

但是如果函数可能返回null,那它的使用者就需要在每一个使用的地方都检查返回值是不是null,这也是为什么Java中有很多的语法来对null进行检查,例如@NonNull, assert a != nullObjects.requireNonNull(a)

函数可以返回null的原因是封装类型总是可以代表两种可能的值,null 或者正常值。这个知识点是隐式的,而且很容易被忘记。更糟的是,即使我们忘记了检查null,编译器也不会提醒我们。

在函数式编程中,我们不使用null。那么通过函数的签名就很容易知道函数的返回类型,不需要去猜它会不会返回null

那么null在函数式编程中就成为了一个side-effect,我们需要一种方法来更显式的表达null所代表的场景。Vavr定义了Option来消除null

上面的例子可以被重构成下面的样子

public Option<Integer> getHead(java.util.List<Integer> intList) {
    if (intList.size() == 0) {
        return Option.none();
    } else {
        return Option.some(intList.get(0));
    }
}

@Test
public void consumerAlwaysKnowOptionMaybeSomeOrNone() {
    Option<Integer> head = getHead(new java.util.LinkedList<Integer>());

    String result = head.fold(() -> "No Element", (x) -> x.toString());

    assertThat(result).isEqualTo("No Element");
}

我们可以用Some来封装正常值,None来替代null

Option.some(1);
Option.none();

我们可以对Option的值进行比较

assertThat(Option.some(1)).isEqualTo(Option.some(1));
assertThat(Option.none()).isEqualTo(Option.none());

也可以将封装的数据提取出来

assertThat(Option.some(1).get()).isEqualTo(1);
assertThat(Option.none().getOrNull()).isNull();

Either

下面的函数是纯函数么?

public Integer divide(Integer x, Integer y) {
    if (y == 0) {
        throw new Error("The denominator can not be 0");
    } else {
        return x / y;
    }
}

这里Error是一个side-effect,因为从函数的返回类型我们无法看出这个函数会抛出Error。我们必须添加注释或者throws关键字来提供更多的信息。但是对于使用者来说,还是不能方便直接的处理这个Error。

为了使这个函数成为纯函数,我们可以使用Either,下面是重构后的函数

public Either<Error, Integer> divide(Integer x, Integer y) {
    if (y == 0) {
        return Either.left(new Error("The denominator can not be 0"));
    } else {
        return Either.right(x / y);
    }
}

Either 可以表示两种不同的数据类型,以StringInteger为例

Either<String, Integer> errorOrInteger = Either.left("Error");
Either<String, Integer> errorOrInteger = Either.right(1);

我们可以比较它的值

assertThat(Either.right(1)).isEqualTo(Either.right(1));
assertThat(Either.left("Error")).isEqualTo(Either.left("Error"));

也支持提取出封装的数据

@Test
public void shouldReturnWrappedValueForRight() {
    Either<String, Integer> intValue = Either.right(1);
    assertThat(intValue.get()).isEqualTo(1);

    Either<String, Integer> intValue2 = Either.left("Error");
    assertThatThrownBy(() -> intValue2.get()).isInstanceOf(NoSuchElementException.class)
            .hasMessageContaining("get() on Left");
}

@Test
public void shouldReturnWrappedValueForLeft() {
    Either<String, Integer> intValue = Either.left("Error");
    assertThat(intValue.getLeft()).isEqualTo("Error");

    Either<String, Integer> intValue2 = Either.right(1);
    assertThatThrownBy(() -> intValue2.getLeft()).isInstanceOf(NoSuchElementException.class)
            .hasMessageContaining("getLeft() on Right");
}

Try

有的时候我们会调用第三方库,而第三方库可能会抛出异常,例如

public Integer add(String x, String y){
   Integer xInt = Integer.valueOf(x);
   Integer yInt = Integer.valueOf(y);
   return xInt + yInt;
}

@Test
public void stringToIntegerMayThrowError() {
    assertThatThrownBy(() -> {
        add("1", "a");
    }).isInstanceOf(IllegalArgumentException.class);
}

我们可以使用正则表达式来检查字符串是否为一个整数,但更简单的做法是用Try

public Try<Integer> add(String x, String y) {
    Try<Integer> xInt = Try.of(() -> Integer.valueOf(x));
    Try<Integer> yInt = Try.of(() -> Integer.valueOf(y));
    return xInt.flatMap(xv -> yInt.map(yv -> xv + yv));
}
@Test
public void tryCanHandleError() {
    assertThat(add("1", "a").getCause()).isInstanceOf(IllegalArgumentException.class);
}

Try可以接受一个函数,当函数抛出异常时,对应的结果是Failure,当函数正常返回时,对应的结果是Success

它当然也可以接受一个常量

Try<Integer> = Try.success(1);
Try<Integer> = Try.failure(new Error("Error"));

还可以提取封装的数据

assertThat(Try.success(1).get()).isEqualTo(1);
assertThatThrownBy(() -> {
    Try.success(1).getCause();
}).isInstanceOf(UnsupportedOperationException.class);
assertThat(Try.failure(new Error("Error")).getCause()).isInstanceOf(Error.class);
assertThatThrownBy(() -> {
    Try.faiure(new Error("Error")).get();
}).isInstanceOf(Error.class);

如何用函数进行编程?

在面向对象编程中,有很多有名的设计模式供我们使用。

在函数式编程中,有很多有名的高阶函数供我们使用。

考虑到我们使用ADT来封装effect,为了便于理解,在后面的章节,我们会用容器来作为ADT的别名。

map

map 可以在容器上调用一个lambda函数,例如

assertThat(Option.some(1).map(x -> x + 1)).isEqualTo(2);
assertThat(Option.none().map(x -> x + 1)).isEqualTo(Option.none());

assertThat(Either.right(1).map(x -> x + 1)).isEqualTo(Either.right(2));
assertThat(Either.left("Error").map(x -> x + 1)).isEqualTo(Either.left("Error"));

assertThat(Try.success(1).map(x -> x + 1)).isEqualTo(Try.success(2));
assertThat(Try.failure(new Error("Error")).map(x -> x + 1).isFailure()).isTrue();

assertThat(List.of(1,2,3).map(x -> x + 1)).isEqualTo(List.of(2,3,4));

Vavr使用传统的class method来实现map,但我们可以把它抽象成一个高阶函数,伪代码如下

public <M<_>, A, B> M<B> map(M<A> value, Function<A, B> f)

这段代码无法在Java中编译,M<_> 代表需要一个类型参数的类型,例如 Option<_>, Either<String, _>, Try<_>, List<_> 等。

flatMap

flatMap 同样可以在容器上调用一个lambda函数,但和map不同的是,它要求lambda函数的返回类型和容器类型相同。如果M.map 接受的函数类型是 A->B,那么 M.flatMap 接受的函数类型是 A -> M<B>

例如

assertThat(Option.some(1).flatMap(x -> Option.some(x + 1))).isEqualTo(2);
assertThat(Option.some(1).flatMap(x -> Option.none())).isEqualTo(Option.none());
assertThat(Option.none().flatMap(x -> Option.some(x + 1))).isEqualTo(Option.none());
assertThat(Option.none().flatMap(x -> Option.none())).isEqualTo(Option.none());

assertThat(Either.right(1).flatMap(x -> Either.right(x + 1))).isEqualTo(Either.right(2));
assertThat(Either.right(1).flatMap(x -> Either.left("Error"))).isEqualTo(Either.left("Error"));
assertThat(Either.left("Error").flatMap(x -> Either.right(x + 1))).isEqualTo(Either.left("Error"));
assertThat(Either.left("Error").flatMap(x -> Either.left("Error2"))).isEqualTo(Either.left("Error"));

assertThat(Try.success(1).flatMap(x -> Try.success(x + 1))).isEqualTo(Try.success(2));
assertThat(Try.success(1).flatMap(x -> Try.failure(new Error("Error"))).isFailure()).isTrue();
assertThat(Try.failure(new Error("Error")).flatMap(x -> Try.sucess(x + 1)).isFailure()).isTrue();
assertThat(Try.failure(new Error("Error")).flatMap(x -> Try.failure(new Error("Error2"))).isFailure()).isTrue();

assertThat(List.of(1,2,3).flatMap(x -> List.of(x, x))).isEqualTo(List.of(1,1,2,2,3,3,4,4));

flatMap 的伪代码如下

public <M, A, B> M<B> flatMap(M<A> value, Function<A, M<B>> f)

filter

filter 是一个flatMap用法的语法糖,以 Option 为例

public <A> Option<A> filter(Option<A> value, Function<A, boolean> f) {
	return value.flatMap(x -> {
		return f(x) ? Option.some(x) : Option.none();
	});
}

不同容器的filter有着不同的行为,例如

assertThat(Option.some(1).filter(x -> x > 1)).isEqualTo(Option.none());
assertThat(Option.some(1).filter(x -> x < 2)).isEqualTo(Option.some(1));
assertThat(Option.none().filter(x -> x > 1)).isEqualTo(Option.none());
assertThat(Option.none().filter(x -> x < 2)).isEqualTo(Option.none());

assertThat(Either.right(1).filter(x -> x > 1)).isEqualTo(Option.some(Either.right(1)));
assertThat(Either.right(1).filter(x -> x < 2)).isEqualTo(Option.none());
assertThat(Either.left("Error").filter(x -> x > 1)).isEqualTo(Option.none());
assertThat(Either.left("Error").filter(x -> x < 2)).isEqualTo(Option.none());

assertThat(Try.success(1).filter(x -> x > 1).isFailure()).isTrue();
assertThat(Try.success(1).filter(x -> x < 2)).isEqualTo(Try.success(1));
assertThat(Try.failure(new Error("Error")).filter(x -> x > 1).isFailure()).isTrue();
assertThat(Try.failure(new Error("Error")).filter(x -> x < 2).isFailure()).isTrue();

assertThat(List.of(1,2,3).filter(x -> x > 1)).isEqualTo(List.of(2,3));

foldLeft

foldLeft 可以将容器所封装的数据转化为一个同类型的值,例如

assertThat(List.of(1,2,3).foldLeft(0, (acc, ele) -> acc + ele)).isEqualTo(6);

但是在Vavr中,OptionEither,和 Try 只实现了 fold 函数

assertThat(Option.some(1).fold(() -> "none", x -> x.toString())).isEqualTo("1");
assertThat(Option.none().fold(() -> "none", x -> x.toString())).isEqualTo("none");

assertThat(Either.right(1).<String>fold((e) -> "left", x -> x.toString())).isEqualTo("1");
assertThat(Either.left("Error").<String>fold((e) -> "left", x -> x.toString())).isEqualTo("left");

assertThat(Try.success(1).<String>fold((e) -> "failure", x -> x.toString())).isEqualTo("1");
assertThat(Try.failure(new Error("Error")).<String>fold((e) -> "failure", x -> x.toString()))
                .isEqualTo("failure");

其实它们也可以实现foldLeft, 以 Option 为例

public <A, B> B foldLeft(Option<A> option, B initial, Function2<B, A, B> f){
	return option.flatMap(x -> f(initial, x)).getOrElse(initial);
}

public <A, B> B fold(Option<A> option, B onNone, Function<A, B> onSome){
	return foldLeft(option, onNone, (acc, ele) -> onSome(ele));
}

foldLeft 的伪代码为

public <M<_>, A, B> B foldLeft(M<A> value, B initial, Function2<B, A, B> f)

如何提取容器中的数据?

为了消除函数的side-effect,我们使用了一个接口作为函数所有effect的父类,每一个effect都是这个接口的子类。对于函数的使用者来说,为了确定函数返回的是哪一个effect,我们需要找到一种方法来区分每一个effect,然后执行对应的业务逻辑。

对于Option,我们可以使用 isSomeisNone

assertThat(Option.some(1).isSome()).isTrue();
assertThat(Option.some(1).isNone()).isFalse();

对于Either,我们可以使用 isRightisLeft

assertThat(Either.right(1).isRight()).isTrue();
assertThat(Either.right(1).isLeft()).isFalse();

对于Try,我们可以使用 isSuccessisFailue

assertThat(Try.success(1).isSuccess()).isTrue();
assertThat(Try.success(1).isFailure()).isFalse();

显而易见,我们的代码里会有很多的if-else,例如

if(v.isSome()){
   ...
} else {
   ...
}

Vavr提供了pattern matching来帮助我们避免这种情况。

Pattern Matching

Vavr的pattern matching是受Scala Pattern Matching的启发,是一个非常强大的工具。

我们可以使用它来提取容器的数据

@Test
public void canMatchOption() {
    Integer result =
            Match(Option.some(1)).of(Case($Some($()), x -> x + 10), Case($None(), 2));

    assertThat(result).isEqualTo(11);
}

@Test
public void canMatchEither() {

    Either<String, Integer> eitherValue = Either.right(1);

    String result = Match(eitherValue).of(Case($Right($()), x -> "right"),
            Case($Left($()), x -> "left"));

    assertThat(result).isEqualTo("right");

}

@Test
public void canMatchTry() {
    Try<Integer> tryValue = Try.success(1);

    String result = Match(tryValue).of(Case($Success($()), x -> "success"),
            Case($Failure($()), x -> "failure"));

    assertThat(result).isEqualTo("success");
}

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

    Integer result = Match(listValue).of(Case($Cons($(), $()), (head, tail) -> head));

    assertThat(result).isEqualTo(1);
}

也可以检查数据是否满足给定的条件

@Test
public void canCheckValue() {
    Integer intValue = 2;
    String result = Match(intValue).of(Case($(x -> x > 0), x -> "positive"),
            Case($(x -> x < 0), x -> "negative"),
            Case($(x -> x == 0), x -> "zero"));

    assertThat(result).isEqualTo("positive");
}

还可以像switch一样使用

@Test
public void canMatchLikeSwitch() {
    Integer intValue = 1;

    String result = Match(intValue).of(Case($(1), "1"), Case($(2), "2"), Case($(), "-1"));

    assertThat(result).isEqualTo("1");
}

总结

Vavr的很多概念都来自于Scala,如果你对函数式编程有兴趣,可以尝试一下Scala里的cats ,它提供了很多更加强大的功能。