使用 Either 处理流中的异常

Exception handling in streams with Either

背景

我一直着迷于 Scott WLaschin's Railway Oriented Programming 处理异常的模型:有一个侧通道,所有坏的东西都会被处理,而好的东西留在主轨道上。下图:

问题

日常代码中出现的常见模式如下:

问题

如何以类似于上面讨论的面向铁路的模型的方式执行此操作。

处理“旁路”异常的一种简单方法是使用 Vavr 提供的 peekleft 方法,该方法消耗 Either.Left() 侧。我们可以将我们的异常处理逻辑插入其中,并将我们的 Either.right() 东西很好地留在主轨道上,没有任何丑陋。

我很确定这可以改进,并且会喜欢改进它的想法。

List<String> errors = new ArrayList<>();

@Test
public void composeExceptions() {

    List<Integer> valids = IntStream.range(0, 11).boxed()
            .map(this::validate)             // throws exceptions
            .peek(this::handleExceptions)    // process left/exceptions on the side
            .flatMap(Value::toJavaStream)    // flatmap is right based
            .collect(Collectors.toList());

    System.out.println("========= Good ones =========");
    System.out.println(valids);
    System.out.println("========= Bad Ones =========");
    errors.forEach(System.out::println);

}

public void handleExceptions(Either<IllegalArgumentException, Integer> either) {
    either.peekLeft(e -> errors.add(e.getMessage())); // is this a monadic bind ???
}

public Either<IllegalArgumentException, Integer> validate(Integer i) {
    if (i % 2 == 0) return Either.right(i);
    return Either.left(new IllegalArgumentException("odd one's out : " + i));
}

[Somjit 的][1] 答案显示了正确的方法,但它使用外部变量 (errors) 来累积错误。一般来说,这是不鼓励的,因为我们应该避免 global/external 状态。

您可以使用 vavr 的 [partition][2] 方法将流分成两部分:一个带有错误,一个带有经过验证的整数。它们都被放入一个元组中:

public void composeExceptions() {
   final Tuple2<Stream<Either<IllegalArgumentException, Integer>>, Stream<Either<IllegalArgumentException, Integer>>> both = Stream.range(1, 11)
            .map(this::validate)
            .partition(Either::isLeft);

   both._1.map(Either::getLeft).forEach(e -> System.out.println("Got error: " + e.getMessage()));
   both._2.map(Either::get).forEach(i -> System.out.println("Validated correctly: " + i));
}

编辑 实际上还有其他选项,例如:

Stream
   .range(1, 11)
   .map(this::validate)
   .toJavaStream()
   .collect(Collectors.teeing(
      Collectors.filtering(Either::isLeft, toList()),
      Collectors.filtering(Either::isRight, toList()),
      (errors, ints) -> new Tuple2<>(errors.stream().map(Either::getLeft), ints.stream().map(Either::get))));

使用 teeing,这是来自 java API 的非常有趣的收集器。不幸的是,它混合了 vavr 和 java API 这不是很好,也不可怕。

并且:

Stream
   .range(1, 11)
   .map(this::validate)
   .collect(
      () -> new Tuple2<>(List.<RuntimeException>empty().asJavaMutable(), List.<Integer>empty().asJavaMutable()),
      (tuple, either) -> {
         either.peekLeft(tuple._1::add);
         either.peek(tuple._2::add);
      },
      (t1, t2) -> {
         t1._1.addAll(t2._1);
         t1._2.addAll(t2._2);
      }
    )
       .map((exceptions, integers) -> new Tuple2<>(List.ofAll(exceptions), List.ofAll(integers)));```

which uses vavr API only but underneath uses java `List` since a mutable structure is required here.

  [1]: 
  [2]: https://www.javadoc.io/doc/io.vavr/vavr/latest/io/vavr/collection/Traversable.html#partition(java.util.function.Predicate)