2

I'm having some trouble understanding the validation library, io.vavr.control.Validation. At the risk of asking too broad a question, I do have several sub-questions—however I believe they are closely related and would piece together to help me understand the proper way to use this validation mechanism.

I started with the example here: https://softwaremill.com/javaslang-data-validation.

Validation<String, ValidRegistrationRequest> validate(RegistrationRequest request) {
   return combine(
       validateCardId(request.getCardId()),
       validateTicketType(request.getTicketType()),
       validateGuestId(request.getGuestId())
)
       .ap(ValidRegistrationRequest::new)
       .mapError(this::errorsAsJson);
}

private Validation<String, Card> validateCardId(String cardId) {
    // validate cardId
    // if correct then return an instance of entity the cardId corresponds to
}

private Validation<String, TicketType> validateTicketType(String ticketType) {
    // validate ticketType
    // if known then return enumeration representing the ticket
}

private Validation<String, Guest> validateGuest(String guestId) {
    // validate guestId
    // if correct then return an instance of entity the questId corresponds to
}

At first, I didn't understand where the generic parameters for Validation<String, ValidRegistrationRequest> came from. I now understand that they are linked to the return types of the methods passed to mapError and ap, respectively. But:

  1. How does combine know to return Validation<String, ValidRegistrationRequest>? I feel the only way this is possible, is if combine is actually a Validation<String, ValidRegistrationRequest>::combine, so that the ap and mapError are defined from this template. But I don't believe that the compiler should be able to imply that that combine refers to a static implementation in the class of the return type. What's happening here?

  2. [Minor] What is the use case for using a ValidRegistrationRequest as opposed to just RegistrationRequest again? I'm tempted to do the latter in my coding, until I see an example.

A second example I was reading about is here: http://www.vavr.io/vavr-docs/#_validation.

class PersonValidator {

    private static final String VALID_NAME_CHARS = "[a-zA-Z ]";
    private static final int MIN_AGE = 0;

    public Validation<Seq<String>, Person> validatePerson(String name, int age) {
        return Validation.combine(validateName(name), validateAge(age)).ap(Person::new);
    }

    private Validation<String, String> validateName(String name) {
        return CharSeq.of(name).replaceAll(VALID_NAME_CHARS, "").transform(seq -> seq.isEmpty()
                ? Validation.valid(name)
                : Validation.invalid("Name contains invalid characters: '"
                + seq.distinct().sorted() + "'"));
    }

    private Validation<String, Integer> validateAge(int age) {
        return age < MIN_AGE
                ? Validation.invalid("Age must be at least " + MIN_AGE)
                : Validation.valid(age);
    }

}
  1. Where did Seq come from? Is that the default when no mapError is supplied? But I'm looking at the decompiled .class file for Validation.class, and the only reference to Seq is here:

      static <E, T> Validation<List<E>, Seq<T>> sequence(Iterable<? extends Validation<List<E>, T>> values) {
        Objects.requireNonNull(values, "values is null");
        List<E> errors = List.empty();
        List<T> list = List.empty();
        Iterator var3 = values.iterator();
    
        while(var3.hasNext()) {
          Validation<List<E>, T> value = (Validation)var3.next();
          if (value.isInvalid()) {
            errors = errors.prependAll(((List)value.getError()).reverse());
          } else if (errors.isEmpty()) {
            list = list.prepend(value.get());
          }
        }
    
        return errors.isEmpty() ? valid(list.reverse()) : invalid(errors.reverse());
      }
    

    Which, I don't think is relevant. Perhaps I'm using an outdated Validation? (It is after all javaslang.control.Validation in my imports, not io.vavr.control.Validation.)

  2. I had this question for both examples: How does combine know which parameters to pass to the constructor (ap), and in what order? Is the answer, "All its parameters, in the order given"?

Thanks in advance.

Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145

2 Answers2

9

You have the same questions and doubts I had when was looking for the first time into validation mechanism of Vavr.

Here are my responses to the first two questions:

  1. combine(...) method returns with an instance of a validation builder, in this case, this is a Builder3 class holding three results of validate*(...) functions. The ap(...) method is a method of this builder and triggers building of Validation instance.

When it is called, validation results are applied, one by one, to a curried version of a function provided as an argument:

v3.ap(v2.ap(v1.ap(Validation.valid(f.curried()))))

In the example, f is a constructor of ValidRegistrationRequest class. In the end, we have a validation holding the valid request instance.

On the other hand, if any of the results are invalid, the method creates an invalid result with a list of error messages. And calling mapError(this::errorsAsJson) (on Validation instance this time!) transforms it into a JSON format.

  1. What's the use case of using ValidRegistrationRequest?

I have used Vavr's validation in one of my projects. I had a request coming with some identifiers of entities. To validate the correctness of it, I had to query a database to check whether there is something for each id.

So, if validation returned with the original request, I would have to fetch those objects from the database once again. Thus, I decided to return ValidRegistrationRequest holding domain objects. With calling database once only, request processing is significantly faster.

And answers to the second pair of questions:

  1. Yes, you are right. In case of an invalid result, Validation.combine(...).ap(...) returns with an instance of Invalid class, holding a list of error messages, returned from validation methods.

If you look into sources, to Validation.ap(...) method, you can see that invalid results are gathered into a Vavr's List. Because it inherits from Seq, you can see this type in the validatePerson example Seq<String>.

  1. Yes, exactly. "All its parameters, in the order given" :)

The order of arguments in combine must be the same as the order of arguments taken by the function provided to ap(...) method.

With sources downloaded, it is way easier to track internals of Vavr.

  • Thanks so much! I saw the "currying" methods and didn't know what to make of them, but now it's clear, as are all my questions. – Andrew Cheong Jan 26 '18 at 16:32
  • May I ask a followup question? Is it necessary to use `ap`? My `RegistrationRequest` is a very large and complex object that doesn't have a straightforward constructor. Neither do I need to make any database or otherwise slow lookups to do my validation. I just want to check certain fields for non-emptiness, certain DateTimes for sensible values, accumulate any errors, and in the end, go one path if it's `Valid`, another path if it's `Invalid`. It seems like the `BuilderN<>` doesn't help me here. Can't I just reuse my `RegistrationRequest`? Am I using the wrong tool for the job? – Andrew Cheong Jan 26 '18 at 17:06
  • Okay, so after talking with a senior engineer at my company, the answer to my follow-up is: Yes, in our case we have no need for `combine` or `ap`, because we have no desire to reconstruct from the validated results. I asked, "We only need an aggregator then?" He confirmed. I asked, "Isn't this library a bit overkill then?" He agreed. It appears someone before just wanted to try using this library, and it stuck. – Andrew Cheong Jan 26 '18 at 19:39
  • 1
    The `ap` method is the substantial one. Without calling it, you won't be able to say whether your request is valid or not. And it looks like some other validation tool would be better than Vavr's solution in your situation. Can you use [Java Bean Validation (JSR 380)](http://www.baeldung.com/javax-validation) maybe? – Michał Chmielarz Jan 26 '18 at 19:43
0

Okay, this is my attempt at answering my own questions, but confirmation from someone more experienced would be nice. I found the latest source for Validation here.

Example 1

  1. The article I copied the example from actually stated that combine was "statically imported for better readability." I missed that. So, I was right—we are calling a static method. Specifically, this one:

    static <E, T1, T2, T3> Builder3<E, T1, T2, T3> combine(Validation<E, T1> validation1, Validation<E, T2> validation2, Validation<E, T3> validation3) {
        Objects.requireNonNull(validation1, "validation1 is null");
        Objects.requireNonNull(validation2, "validation2 is null");
        Objects.requireNonNull(validation3, "validation3 is null");
        return new Builder3<>(validation1, validation2, validation3);
    }
    
  2. My guess at the use of ValidRegistrationRequest is simply to enforce validation at compile-time. That is, this way, a developer can never accidentally use an unvalidated RegistrationRequest if all consuming code require a ValidRegistrationRequest.

Example 2

  1. I think the Set comes from here:

    /**
     * An invalid Validation
     *
     * @param <E> type of the errors of this Validation
     * @param <T> type of the value of this Validation
     */
    final class Invalid<E, T> implements Validation<E, T>, Serializable {
    
        ...
    
        @Override
        public Seq<E> getErrors() {
            return errors;
        }
    
        ...
    }
    

    And then something to do with this:

    /**
     * Applies a given {@code Validation} that encapsulates a function to this {@code Validation}'s value or combines both errors.
     *
     * @param validation a function that transforms this value (on the 'sunny path')
     * @param <U>        the new value type
     * @return a new {@code Validation} that contains a transformed value or combined errors.
     */
    @SuppressWarnings("unchecked")
    default <U> Validation<E, U> ap(Validation<E, ? extends Function<? super T, ? extends U>> validation) {
        Objects.requireNonNull(validation, "validation is null");
        if (isValid()) {
            return validation.map(f -> f.apply(get()));
        } else if (validation.isValid()) {
            return (Validation<E, U>) this;
        } else {
            return invalidAll(getErrors().prependAll(validation.getErrors()));
        }
    }
    
    1. @mchmiel answered my question while I was writing mine.
Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145