vavr-io / vavr

vʌvr (formerly called Javaslang) is a non-commercial, non-profit object-functional library that runs with Java 8+. It aims to reduce the lines of code and increase code quality.
https://vavr.io
Other
5.67k stars 628 forks source link

Applicative validation - Sequence actions, discarding left or right #2626

Open CauchyPeano opened 3 years ago

CauchyPeano commented 3 years ago

I am really enjoying using Validation class provided by vavr library.

But sometimes it is tedious to build validating logic for common use cases in Java.

Right now main use-case for Validation is to use it together with calls to combine. This allows us to collect all successful validation and pass them to constructor function.

But usually in my experience you can already get an object from parser library and want to run validation over it. Then validating code might look like this:

    public static void main(String[] args) {
        Person bob = new Person("Bob", "+12345", "bob@test.com");
        Validation<Seq<String>, Person> result = Validation.<String, Person>valid(bob)
            .combine(validateEmail(bob))
            .combine(validatePhone(bob))
                .ap((a, b, c) -> a);
    }

    public static Validation<String, Person> validateEmail(Person p) {
        if (p.email == null) return Validation.invalid("Null email!"); else return Validation.valid(p);
    }

    public static Validation<String, Person> validatePhone(Person p) {
        if (p.phoneNumber == null || !p.phoneNumber.startsWith("\\+")) return Validation.invalid("Wrong phone!"); else return Validation.valid(p);
    }

If validation grows bigger, then we can use Validation.sequence. But still we need to provide a function to discard other results, e.g. reduce((a, b) -> a).

What is missing there are useful functions that have equivalence in Haskell (couldn't find them in scalaz) :

(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a

Then code above might look like this:

    public static void main(String[] args) {
        Person bob = new Person("Bob", "+12345", "bob@test.com");
        Validation<Seq<String>, Person> result = Validation.<Seq<String>, Person>valid(bob)
            .vleft(validateEmail(bob))
            .vleft(validatePhone(bob));
    }

This will make Validation much easier to use and easier to adopt. I am still not sure how to name those functions.

danieldietrich commented 3 years ago

I love this idea ❤️ It is the simplest form of validation I can imagine!

I updated this comment, because we can't use flatMap. That idea was nonsense and my suggestion was even false.

What about the more flexible version with a higher order function?

-- equivalent to (<*)
andThen :: f a -> (a -> f b) -> f a

-- a generalization of (*>)
-- (<*) could be also expressed with it by setting c to a
andThenFold :: f a -> (a -> f b) -> (a -> b -> c) -> f c

Example:

// type Validation<String, BasicAuth>
var result = Validation.valid(new BasicAuth("user", "pass"))
    // type Validation<String, User>
    .andThenFold(App::validateCredentials, (basicAuth, grants) -> new User(basicAuth.user, grants))
    // type Validation<String, User>
    .andThen(App::validateUserGrants);

In Typelevel Cats, there exists andThen as part of Sequential Validation. But in our case, we would additionally accumulate errors.

CauchyPeano commented 3 years ago

Hi Daniel,

I am very glad that you liked this idea!

Yeah, flatMap idea was a bit confusing to me :)

I think andThen and andThenFold will work out really good. Name fits well. Also we don't need both right *> and left <* operations - only one is enough. If you don't mind I can make PR and maybe some examples as PoC.

Happy new year 🎉

mattfara commented 2 years ago

I'm not sure how I'd do validation against more than eight fields. I wind up with a List<Validation<String,Long>> of arbitrary length but I don't know how to get it into Validation<Seq,Long>. @CauchyPeano

If validation grows bigger, then we can use Validation.sequence

Is that what would apply in my case? I can't understand how to use it here.

CauchyPeano commented 2 years ago

@mattfara Sequence is really handy to work with arbitrary number of validations, but you need then to update validating functions accordingly.

    public static void main(String[] args) {
        Person person = new Person("Bob", "+12345", "bob@test.com");

        Validation<Seq<String>, Seq<Person>> sequence =
                Validation.sequence(List.of(validateEmail(person), validatePhone(person)));
        // person will contain same reference, taking either p1 or p2, they still the same
        Validation<Seq<String>, Person> result = sequence.map(seqP -> seqP.reduce((p1, p2) -> p2)); 
    }

    public static Validation<Seq<String>, Person> validateEmail(Person p) {
        if (p.email == null) return Validation.invalid(List.of("Null email!"));
        else return Validation.valid(p);
    }

    public static Validation<Seq<String>, Person> validatePhone(Person p) {
        if (p.phoneNumber == null || !p.phoneNumber.startsWith("\\+"))
            return Validation.invalid(List.of("Wrong phone!"));
        else return Validation.valid(p);
    }    
mattfara commented 2 years ago

Thank you for the help!

Sent from my iPhone

On Jan 12, 2022, at 4:31 PM, Igor Konoplyanko @.***> wrote:

 @mattfara Sequence is really handy to work with arbitrary number of validations, but you need then to update validating functions accordingly.

public static void main(String[] args) {
    Person person = new Person("Bob", "+12345", ***@***.***");

    Validation<Seq<String>, Seq<Person>> sequence =
            Validation.sequence(List.of(validateEmail(person), validatePhone(person)));
    // person will contain same reference, taking either p1 or p2, they still the same
    Validation<Seq<String>, Person> result = sequence.map(seqP -> seqP.reduce((p1, p2) -> p2)); 
}

public static Validation<Seq<String>, Person> validateEmail(Person p) {
    if (p.email == null) return Validation.invalid(List.of("Null email!"));
    else return Validation.valid(p);
}

public static Validation<Seq<String>, Person> validatePhone(Person p) {
    if (p.phoneNumber == null || !p.phoneNumber.startsWith("\\+"))
        return Validation.invalid(List.of("Wrong phone!"));
    else return Validation.valid(p);
}    

— Reply to this email directly, view it on GitHub, or unsubscribe. Triage notifications on the go with GitHub Mobile for iOS or Android. You are receiving this because you were mentioned.