1

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.

Stefan Zobel
  • 3,182
  • 7
  • 28
  • 38
Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145
  • 1
    I think it would be a good idea to describe what you are trying to achieve just using simple English. So you've got a bunch of `Validation` objects and I understand you want to combine them into a single `Validation` object. That means that if any of them is `invalid`, you want to end up in an `invalid` too, but have multiple error messages concatenated in case there are more than one. But it's not clear to me what do you want to do with the multiple `Foo` objects in case they are all `valid`. How do you want to combine them into a single one? – Nándor Előd Fekete Jan 29 '18 at 16:18
  • @NándorElődFekete - Yes, your understanding is spot on. The valid case is just a singleton representing that all (or the single) objects are valid, so they simply collapse. – Andrew Cheong Jan 29 '18 at 16:49

4 Answers4

1

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.

Nándor Előd Fekete
  • 6,988
  • 1
  • 22
  • 47
  • Thank you. Do you think [`Validation`'s `sequence` method](https://github.com/vavr-io/vavr/blob/master/vavr/src/main/java/io/vavr/control/Validation.java) might be of use? I could not understand why it was an `Iterable`, but maybe it was provided for this purpose... – Andrew Cheong Jan 29 '18 at 16:52
  • Actually yes, I somehow missed that method. I'll update my answer. – Nándor Előd Fekete Jan 29 '18 at 17:02
  • Thanks again. Sorry, I'm coming from C++ and having trouble with some basic things. `List::of` doesn't seems to exist. `Array::of` results in an error, `no instance(s) of type variable(s) R exist so that Stream conforms to Iterable extends Validation, T>>`. What is the appropriate function needed for, as I understand, simply creating a list? – Andrew Cheong Jan 29 '18 at 19:55
  • Ah, I see. `List::of` was introduced in Java 9. However I need to stick to Java 8... – Andrew Cheong Jan 29 '18 at 20:05
  • `List` in this case is `io.vavr.collection.List`. Sorry for the confusion, it's just that import statements usually decrease the signal-to-noise ratio so I tend do skip including them in my code snippets on stack overflow. But in this case it would've been better to include it. Actually, you could use any implementation of `io.vavr.collection.Seq` in place of that. – Nándor Előd Fekete Jan 29 '18 at 20:22
  • I think your solution would have worked, conceptually! Accepted. I self-answered with the solution I ended up going with, and why. Thank you very much. – Andrew Cheong Jan 29 '18 at 22:14
0

As I see, the output of your reduce is a string with the list of errors separated by ;.

You are mixing the accumulator parameters:

  • a is a partial result of the current reduction
  • b is the object itself that you are iterating

I'd do something like this:

Stream.of(a, b, c).reduce(
    "", //initial state,
    (prevState, validationObject) -> {
        if (validationObject.isInvalid())
            return prevState + ";" + validationObject.getError();
        else
            return prevState;
    }
)
Diogo Sgrillo
  • 2,601
  • 1
  • 18
  • 28
  • Thank you! I agree with your approach if in the end I could accept a `""` as valid, but I would like to end up with a `Validation`. – Andrew Cheong Jan 29 '18 at 15:49
0

What about Collectors.groupingBy(). I think still you can improve false part set of string and join them, Output of below code is:

{false=[SomeException, ReallyBadProblem], true=[3, 5]}

Code example:

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toSet;

import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Stream;

public class Main {


    public static void main(String[] args)  {

        Validation<String,Integer> a = new Validation<>(true, null, 3);
        Validation<String,Integer> b = new Validation<>(true, null, 5);
        Validation<String,Integer> c = new Validation<>(false, "SomeException", null);
        Validation<String,Integer> d = new Validation<>(false, "ReallyBadProblem", null);

        //Stream.of(a,b,c).collect(Collectors.groupingBy(v->v.isValid(), v->v.));

        TreeMap<Boolean, Set<Object>> map = Stream.of(a,b,c,d).collect((groupingBy(v->v.isValid(), TreeMap::new,
                                             mapping(v-> { return v.isValid() ? v.valid() : v.getError();}, toSet()))));

        System.out.println(map);
    }

    public static class Validation<E, T>{

        boolean valid;
        T validVal;
        String error;


        public Validation(boolean valid, String error, T validVal) {
            super();
            this.valid = valid;
            this.error = error;
            this.validVal = validVal;
        }
        /**
         * @return the valid
         */
        public boolean isValid() {
            return valid;
        }
        /**
         * @param valid the valid to set
         */
        public void setValid(boolean valid) {
            this.valid = valid;
        }
        /**
         * @return the error
         */
        public String getError() {
            return error;
        }
        /**
         * @param error the error to set
         */
        public void setError(String error) {
            this.error = error;
        }

        public T valid() {
            return validVal;
        }


    }
}
HRgiger
  • 2,750
  • 26
  • 37
0

This is the way I ended up going, though I think @Nandor (accepted answer) had it right. (His solution was based on a more current version of io.vavr.control.Validation than what's available to me (still javaslang.control.Validation). I figured out that mapLeft was renamed to mapErrors, but there were some missing bits related to Seq manipulation. Due to lack of familiarity with Java, I was unable to resolve my errors going this route.)

Validation<String, AdRequest> validation =
    Stream.of(
        validateTargetingRequest(adRequest),
        validateFlightRequest(adRequest, ad),
        validateCreativeRequest(adRequest)
    ).reduce(
        Validation.valid(adRequest),
        (a, b) -> {
          if (a.isValid() && b.isValid()) {
            return Validation.valid(adRequest);
          }
          if (a.isInvalid() && b.isInvalid()) {
            return Validation.invalid(String.join("; ", a.getError(), b.getError()));
          }
          // This seemingly overcomplicated structure was necessitated by the fact that getError
          // throws an exception when called on an Valid form of Validation.
          return a.isInvalid() ? a : b;
        }
    );
Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145