List和Option

303 阅读3分钟

我们已经在 Vavr Introduction中介绍了Vavr。 在日常工作中我们经常会碰到使用ListOption 的情况,这篇博客将会讨论一些常见场景。

将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给过滤掉,并且把IntegerOption中拿出来。例如

//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)

上面的代码虽然可以工作,但我们必须把filtermap一起使用。并且Option::get可能抛出异常,如果将来我们在filtermap之间加入了其他的操作,这段代码可能会抛出未知的异常。

理想的做法是将filtermap合并成一个原子操作,而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>>

如何才能调换OptionList的位置呢?我们可以这样做

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::traverseOption::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 进行尝试。