使用 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)
背景
我一直着迷于 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)