我们已经在 Vavr Introduction中介绍了Vavr。 在日常工作中我们经常会碰到使用List 和 Option 的情况,这篇博客将会讨论一些常见场景。
将List转换为Option
有时候我们希望拿到List的第一个元素
List<Integer> list = List.of(1,2,3);
Integer head = list.get(0); // head = 1
但是List可能为空,那么在拿第一个元素之前我们需要检查List的长度
Integer head = 0; // default value
if(list.size() > 0) {
head = list.get(0); // head = 1
}
在上面的代码中,我们将head的默认值设为0。如果我们在取第一个元素时不知道该设什么默认值,怎么办?
有两个办法,要么将List一直传下去直到知道默认值,要么先设一个特殊的默认值,后面再用一个合理的值来替换它。
public Integer processHead(List<Integer> list) {
Integer head = 2; // reasonable default value
if(list.size() > 0){
head = list.get(0) + 1;
}
return head;
}
public Integer processHead(Integer head) {
if(head == -1) { // special default value
return 2; // reasonable default value
}
return head + 1;
}
为了避免长度检查和特殊的默认值,我们可以使用List::headOption来取第一个元素
List<Integer> list = List.of(1,2,3);
Option<Integer> head = list.headOption(); // Option.Some(1)
public Integer processHead(Option<Integer> head) {
return head.map(x -> x + 1).getOrElse(2);
}
当List.size() > 0 时,List::headOption 返回 Option.Some。当List.size() == 0时,List::headOption返回Option.None。而Option允许我们在完成所有操作后,使用getOrElse设置一个合理的默认值。
将Option转换为List
假设我们有一个函数sum,用来计算List<Integer>中元素的和。如果我们现在只有一个Option对象,怎么办?能把Option转换成List么?
我们可以这样实现
public Integer sum(List<Integer> list) {
return list.fold(0, (acc, ele) -> acc + ele);
}
Option<Integer> option = Option.some(1);
List<Integer> convertedList = option.isDefined() ? List.of(option.get()) : List.empty(); // List(1)
Integer sumValue = sum(convertedList); // 1
但Vavr提供了一种更简洁的方法
Option<Int> option = Option.some(1);
Int sumValue = sum(List.ofAll(option)); // 1
List::ofAll 可以将任意一个Iterable转换为List。幸运的是,Option就是Iterable的子类。
将List<Option>转换为List
假设我们有一个List,它的元素是Option<Integer>。现在我们需要把None给过滤掉,并且把Integer从Option中拿出来。例如
//current
List<Option<Integer>> currentList = List.of(Option.some(1), Option.none(), Option.some(2), Option.none());
//expected
List<Integer> expectedList = List.of(1, 2);
最直接的实现是
List<Integer> expectedList = currentList.filter(Option::isDefined).map(Option::get); // List(1, 2)
上面的代码虽然可以工作,但我们必须把filter和map一起使用。并且Option::get可能抛出异常,如果将来我们在filter和map之间加入了其他的操作,这段代码可能会抛出未知的异常。
理想的做法是将filter和map合并成一个原子操作,而flatMap可以满足我们的需求。
List<Integer> expectedList = currentList.flatMap(x -> x); // List(1, 2)
为什么这段代码可以工作呢?Vavr扩展了flatMap的实现,它可以接受的函数类型为A -> Iterable<B>,Option正好是Iterable的子类。
default <U> List<U> flatMap(Function<? super T, ? extends Iterable<? extends U>> mapper)
我们不仅能够将List<Option<A>>转换为List<A>,而且可以在List<A>上直接应用一个函数A -> Option<B>,例如
List<Integer> list = List.of(1, 2, 3, 4);
public Option<Integer> isOdd(Integer v) {
if(v % 2 == 0){
return Option.none();
}
return Option.some(v);
}
List<Integer> oddList = list.flatMap(this::isOdd); // List(1, 3)
但并不是所有的函数式编程库都支持这个操作,例如cats。原因是这种实现没有严格遵守Monad的定义。不过在实践过程中,这种实现真的非常方便。
将List<Option>转换为Option<List>
假设有一列id,我们要通过id获取用户名,然后要么返回全部用户名,要么什么都不返回。
public Option<String> fetchName(Integer id) {
return Option.some("name" + id);
}
List<Integer> ids = List.of(1, 2, 3, 4, 5, 6);
Option<List<String>> names = ids.map(this::fetchName); // compile error, can't assign List<Option<String>> to Option<List<String>>
如何才能调换Option和List的位置呢?我们可以这样做
Option<List<String>> names =
ids.map(this::fetchName).foldLeft(Option.some(List.empty()), (acc, ele) -> {
if(ele.isEmpty() || acc.isEmpty()){
return Option.none();
}
return acc.map(list -> list.append(ele.get()));
});
但是这里使用Option::traverse和Option::sequence会更加简洁
Option<List<String>> names = Option.traverse(ids, this::fetchName);
Option<List<String>> names = Option.sequence(ids.map(this::fetchName));
总结
我们讨论了一些使用List和Option的常见场景,这些场景也适用于Either和Try。欢迎下载vavr-examples 进行尝试。