Exception handling in streams with Either

Viewed 653

Background

I have been fascinated with Scott WLaschin's Railway Oriented Programming model of handling exceptions: have a side channel, where all the bad stuff will be processed, and keep the good stuff on the main track. Picture below:

enter image description here

Problem

A common pattern that comes up in daily code is like :

  • Have a list of data
  • Validate each one of them
  • The validation can throw exceptions

Question

How to do this in a way that resembles the railway-oriented model discussed above.

2 Answers

[Somjit's][1] answer shows correct approach however it uses an outer variable (errors) to accumulate the errors. This is, in general, discouraged since we should avoid the global/external state.

You can use vavr's [partition][2] method to split the stream into two: one with errors and one with validated ints. They are both put into a tuple:

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));
}

EDIT Actually there also other options like:

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

which uses teeing which is quite interesting collector from java API. Unfortunately it mixes vavr and java API which is not great, not terrible.

And:

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]: https://stackoverflow.com/a/67556075/542270
  [2]: https://www.javadoc.io/doc/io.vavr/vavr/latest/io/vavr/collection/Traversable.html#partition(java.util.function.Predicate)

A simple way of handling the exceptions on the "side track" is by using the peekleft method Vavr provides, which consumes the Either.Left() side. We can plug our exception processing logic in there, and leave our Either.right() stuff nicely on the main track without any ugliness.

I am pretty sure this can be improved, and would love ideas on improving this.

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));
}
Related