Exception handling in streams with Either

1.5k Views Asked by At

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.

3

There are 3 best solutions below

2
On BEST ANSWER

Somjit's 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 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.

0
On

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

I also was fascinated by the presentation pointed out. I will add here some part of the code that I am using for handling checked exception (for these purposes I recommend taking a look at this presentation : Exception Handling in Functional and Reactive Programming by Venkat Subramaniam )

First of all this is how I call it :

import static Monader.fnMaybeToMaybe;
import static Monader.fnOnValue;
import static Monader.fnValueToMaybe;

import java.util.List;

import org.junit.jupiter.api.Test;

public class RailTrackRunner {

    @Test
    public void goingThruSomeClassNames() {

        List<String> classNames = List.of(
                "lombok.Getter",
                "java.util.concurrent.ConcurrentLinkedQueue",
                "dummy.vrs.invalid",
                "lombok.extern.slf4j.Slf4j");

        System.out.println("\nV1");

        classNames.stream()
                .map(Maybe::ofValue)
                .map(fnMaybeToMaybe(Class::forName)) // throws ClassNotFoundException
                .map(fnOnValue(Class::getSimpleName))
                .forEach(System.out::println);

        System.out.println("\nV2");

        classNames.stream()
                .map(fnValueToMaybe(Class::forName)) // throws ClassNotFoundException
                .map(fnOnValue(Class::getSimpleName))
                .forEach(System.out::println);

    }

}

and the necessary pieces are these:

@FunctionalInterface
public interface ExceptionThrower<T, R, EX extends Exception> {

    R apply(T t) throws EX;

}

----- ----- ----- ----- ----- ----- 

import lombok.NonNull;
import lombok.Value;

@Value
public class Maybe<VAL, EXC extends Exception> {

    public static <STVAL, STEXC extends Exception> Maybe<STVAL, STEXC> ofValue(@NonNull STVAL out) {
        return new Maybe<STVAL, STEXC>(out, null);
    }

    public static <STVAL, STEXC extends Exception> Maybe<STVAL, STEXC> ofExc(@NonNull STEXC exc) {
        return new Maybe<STVAL, STEXC>(null, exc);
    }

    private VAL value;
    private EXC exc;

    private Maybe(VAL value, EXC exc) {
        this.value = value;
        this.exc = exc;
        if (!(hasValue() ^ hasException()))
            throw new IllegalArgumentException("Invalid state for Maybe");
    }

    public boolean hasValue() {
        return value != null;
    }

    public boolean hasException() {
        return exc != null;
    }

}

----- ----- ----- ----- ----- ----- 

import java.util.function.Function;

public class Monader<IN, OUT, EX extends Exception> {

    public static <IN, OUT, EX extends Exception> Function<Maybe<IN, EX>, Maybe<OUT, EX>> fnOnValue(Function<IN, OUT> simpleValueFunction) {
        return maybeIn -> {
            if (maybeIn.hasValue()) {
                OUT outValue = simpleValueFunction.apply(maybeIn.getValue());
                return Maybe.ofValue(outValue);
            } else // maybeIn.hasException()
                return Maybe.ofExc(maybeIn.getExc());
        };
    }

    public static <IN, OUT, EX extends Exception> Function<Maybe<IN, EX>, Maybe<OUT, EX>> fnMaybeToMaybe(ExceptionThrower<IN, OUT, EX> exceptionThrower) {
        return maybeIn -> {
            if (maybeIn.hasValue())
                return executeThrower(exceptionThrower, maybeIn.getValue());
            else // maybeIn.hasException()
                return Maybe.ofExc(maybeIn.getExc());
        };
    }

    public static <IN, OUT, EX extends Exception> Function<IN, Maybe<OUT, EX>> fnValueToMaybe(ExceptionThrower<IN, OUT, EX> exceptionThrower) {
        return in -> executeThrower(exceptionThrower, in);
    }

    private static <IN, OUT, EX extends Exception> Maybe<OUT, EX> executeThrower(ExceptionThrower<IN, OUT, EX> exceptionThrower, IN in) {
        try {
            OUT out = exceptionThrower.apply(in);
            return Maybe.ofValue(out);
        } catch (RuntimeException rexc) {
            throw rexc;
        } catch (Exception exc) {
            try {
                @SuppressWarnings("unchecked")
                EX castedEsc = (EX) exc;
                return Maybe.ofExc(castedEsc);
            } catch (ClassCastException cce) {
                throw new RuntimeException(exc);
            }
        }
    }

}