Search code examples
javajava-streamreducefoldvavr

Concise way to reduce where one of the states throws an exception


I have a bunch of these:

Validation<String, Foo> a;
Validation<String, Foo> b;
Validation<String, Foo> c;

Here are some of their methods:

boolean isValid();
boolean isInvalid(); // === !isValid()
String getError();

Now, I was trying to do this:

Stream.of(a, b, c).reduce(
    Validation.valid(foo),
    (a, b) -> a.isValid() && b.isValid()
              ? Validation.valid(foo)
              : String.join("; ", a.getError(), b.getError())
);

There's the obvious issue that if only one of a or b is in error, then there's a needless ;. But there's a more serious issue: getError() throws an exception if the validation is valid.

Is there a way I can write this lambda (or use something else in the io.vavr.control.Validation library) without making all 4 cases (a && b, a && !b, !a && b, !a && !b) explicit?


EDIT

To be clearer, I wanted a result of Validation<String, Foo> in the end. I think it behaves like a "monad," in that way, but I'm not sure.


Solution

  • I think what you're trying to achieve is easier to solve in Either domain.

    First, convert your stream of Validations to a stream of Eithers:

    Stream<Either<String, Foo>> eithers = Stream.of(a, b, c)
        .map(Validation::toEither);
    

    then combine them:

    Either<String, Foo> result = Either.sequence(eithers)
        .mapLeft(seq -> seq.collect(Collectors.joining("; ")))
        .map(combinator); // fill in with combinator function that converts
                          // a Seq<Foo> into a single Foo
    

    Since you didn't specify how you want to combine multiple valid Foo objects into a single one, I left it open for you to fill in the combinator function in the above example.

    Either.sequence(...) will reduce many eithers into a single one by returning an Either.Left containing the sequence of left values if any of the provided eithers is a left, or an Either.Right containing a (possibly empty) sequence of all right values, if none of the provided eithers is a left.

    Update:

    There's a Validation.sequence(...) method that can do it without converting into Either domain (which I somehow missed while creating my original answer -- thanks for pointing out):

    Validation<Seq<String>, Seq<Foo>> validations = Validation.sequence(
            Stream.of(a, b, c)
                .map(v -> v.mapError(List::of))
    );
    
    Validation<String, Foo> result = validations
        .mapError(errors -> errors.collect(Collectors.joining("; ")))
        .map(combinator); // fill in with combinator function that converts
                          // a Seq<Foo> into a single Foo
    

    You said that the Foo instances are the same, that means that you could use Seq::head in place of the combinator function. But you'll need to take care not to use an empty sequence of validations as input as it will cause Seq::head to throw NoSuchElementException in that case.