什么是 Vavr?
Vavr 是一个函数式编程库,支持 Java 8+。它的第一个版本发布于2014年3月9号,叫 Javaslang。
它主要由三部分组成
- Immutable collections,用来避免出现side-effect
- ADT(Algebraic data type),用来消除已存在的side-effect
- 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 != null 和 Objects.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 可以表示两种不同的数据类型,以String和Integer为例
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中,Option,Either,和 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,我们可以使用 isSome 和 isNone
assertThat(Option.some(1).isSome()).isTrue();
assertThat(Option.some(1).isNone()).isFalse();
对于Either,我们可以使用 isRight 和 isLeft
assertThat(Either.right(1).isRight()).isTrue();
assertThat(Either.right(1).isLeft()).isFalse();
对于Try,我们可以使用 isSuccess 和 isFailue
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 ,它提供了很多更加强大的功能。