Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Validation using Applicative Functor #119

Closed
lucapiccinelli opened this issue May 5, 2021 · 14 comments · Fixed by #121
Closed

Support Validation using Applicative Functor #119

lucapiccinelli opened this issue May 5, 2021 · 14 comments · Fixed by #121
Labels
enhancement New feature or request

Comments

@lucapiccinelli
Copy link

lucapiccinelli commented May 5, 2021

Hey there, that's a great library! But I see an issue, similar to the same issue that affects validation via annotations. It is not that simple to define a reusable Value Object class, that is itself composed by other Value Object classes.

Example in Kotlin:

data class Email private constructor(val value: String){
   companion object {
       fun of(value: String): Either<ConstraintViolations, Email> = ValidatorBuilder.of<Email>()
            .konstraint(Email::value) {
                email()
            }.build()
            .validateToEither(Email(emailValue))
   }
}
data class Phone private constructor(val value: String){
   ... similar factory method here
}
data class ContactsInfo(val email: Email, val phone: PhoneNumber){
   companion object {
       fun of(emailValue: String, phoneValue: String): Either<ConstraintViolations, ContactsInfo>{
          val email: Either<ConstraintViolations, Email> = Email.of(emailValue)
          val phone: Either<ConstraintViolations, PhoneNumber> = PhoneNumber.of(phoneValue)
          
          ... How to compose email and phone here to build a ContactsInfo()? ...
       }
   }
}

Build a ConcactsInfo like in the example is not possible as Either lacks of a flatMap method. But also if flatMap was there, it would short-circuit on email, and then we would miss an eventual error on phone, in the list of ConstraintViolations.

I see only two ways out, in the current version of Yavi:

  1. making public the constructors of Email and PhoneNumber and having the of factory method return the ConstraintValidations, by calling validate instead of validateToEither. Then, every time check the value of the isValid property of ConstraintValidations. This solution is not explicit, neither type-safe, and drives to duplication.
  2. Use exceptions.

My solution proposal is: let's discuss a way of integrating Yavi with the Validation Monad of Konad in order to have an extension method like

fun <T> Validator<T>.validateToKonad(target: T): Validation<ConstraintViolations, T> =
    validate(target).run {
        if(isValid) target.success()
        else fail()
    }

With the Validation from Konad the example above would become

data class ContactsInfo(val email: Email, val phone: PhoneNumber){
   companion object {
       fun of(emailValue: String, phoneValue: String): Validation<ConstraintViolations, ContactsInfo> =
           ::ContactsInfo + Email.of(emailValue) + PhoneNumber.of(phoneValue)
   }
}
@making
Copy link
Owner

making commented May 5, 2021

@lucapiccinelli

Thank you for your proposal.
I'm not familiar with Kotlin so let me make sure.

  • Is this a Kotlin specific issue? Can the same be applied to Java?
  • Can your proposal be achieved by modifying an existing Either or introducing another class? I'm not positive about adding dependent libraries.

@lucapiccinelli
Copy link
Author

Hello @making thank you for your kind answer. I will answer your points:

  • Is this a Kotlin specific issue? Can the same be applied to Java? Unfortunately Konad only works for Kotlin. I never went deep on this, because my focus is on Kotlin, but I think that the main incompatibility sits in that way that curry is implemented in Konad. Looks like Kotlin and Java have different internal representations of a function object.
  • Can your proposal be achieved by modifying an existing Either or introducing another class? Theoretically speaking, yes. In this hypothesis, I would suggest not use Either, as it usually has the implicit semantic of short-circuiting on errors. Maybe a Validation class could be a better choice in this case... same as Either, but doesn't short-circuit. Anyway, practically it is a big work because you need to re-implement and maintain applicative functors logics and applicative functors builders from scratch. And if you want to generalize those, you are going to need higher-kinded types implementation. All of this is already implemented in Konad, as its main "core business" is to provide composition with a simple API.
  • I'm not positive about adding dependent libraries. I see... and actually I was expecting it, as it is written as a feature in your README 😅. I would say the same for my library. Maybe the solution here could be having a dedicated maven module that builds its own jar that depends both on Yavi and Konad. Or even provide a totally new repo.

Once having "programmatic" validation and composition together, in the long term I see the possibility of creating a library containing a set of reusable components like Email, Url, PhoneNumber, NotEmptyString, String3, String50... StringN etc... that could even set has a "functional" alternative to jsr 303.

@making
Copy link
Owner

making commented May 10, 2021

@lucapiccinelli I'm positive about adding something similar to Vavr's Validation (maybe Validated to avoid class name collision).
https://github.com/vavr-io/vavr/blob/master/src/main/java/io/vavr/control/Validation.java
Does this solve your case?

@making
Copy link
Owner

making commented May 10, 2021

Because YAVI is before 1.0, breaking API change could happen.
I'm also positive to make validate method returns Validated if it makes sense.

@making
Copy link
Owner

making commented May 12, 2021

@lucapiccinelli
I would like to add the following API. What does it look like? What do you think of the method name?

Validator<Email> emailValidator = ...;
Validator<PhoneNumber> phoneNumberValidator = ...;

Validation<ConstraintViolations, Email> emailValidation = emailValidator.prefixed("email").applicative().validate(email);
Validation<ConstraintViolations, PhoneNumber> phoneNumberValidation = phoneNumberValidator.prefixed("phoneNumber").applicative().validate(phoneNumber);

Validation<ConstraintViolation, ContactInfo> contactInfoValidation = emailValidation
                                                                            .compose(phoneNumberValidation)
                                                                            .apply(ContactInfo:new);
// or

Validation<ConstraintViolation, ContactInfo> contactInfoValidation = Validations.apply(phoneNumberValidation, phoneNumberValidation, ContactInfo:new);

Either<List<ConstraintViolation>, ContactInfo> either = contactInfoValidation.toEither();

making added a commit that referenced this issue May 12, 2021
making added a commit that referenced this issue May 12, 2021
@making
Copy link
Owner

making commented May 12, 2021

@lucapiccinelli I made a pull request (#121) to add Validation class. Can you please take a look at it? Thanks!

@lucapiccinelli
Copy link
Author

Yes thank you. Sorry for the latency. I will tomorrow in the morning!

making added a commit that referenced this issue May 13, 2021
@making
Copy link
Owner

making commented May 13, 2021

BTW, I think YAVI's Arguments Validator can achieve the same outcome though it's more verbose:

Validator<Email> emailValidator = ...;
Validator<PhoneNumber> phoneNumberValidator = ...;

Arguments2Validator<Email, PhoneNumber, ContactInfo> contactInfoValidator = ArgumentsValidatorBuilder.of(ContactInfo::new)
    .builder(b -> b.nest(Arguments1::arg1, "email", emailValidator)
                   .nest(Arguments2::args, "phoneNumber", phoneNumberValidator))
    .build();

Either<ConstraintViolations, ContactInfo> either = contactInfoValidator.validateArgs(email, phoneNumber);

@making making added the enhancement New feature or request label May 13, 2021
@lucapiccinelli
Copy link
Author

Well, that's great. I'm impressed with how quickly you did it. I think that this closes the need that I raised with this issue. Although I would have been very happy to provide an integration of our libraries 😆. Anyway, this is always something that I can do by myself with a dedicated repo 😁.

@lucapiccinelli
Copy link
Author

If you don't mind I would like to let you think about two possible future improvements to this new API.

  1. Instead of the sh scripts to create applicative composition and curry, maybe you could consider using annotation processing. But maybe you already considered it... and decided that sh scripts are just simpler 😅
  2. The new API changes the common order that an OOP developer is used to apply when calling a function. I mean instead of being something like fnName arg1 arg2 ... argN it becomes arg1 arg2 ... argN fnName. For example, in Konad you do ::ContactsInfo + Email.of(emailValue) + PhoneNumber.of(phoneValue), or ::ContactsInfo.curry().on(Email.of(emailValue)).on(PhoneNumber.of(phoneValue)).validation and both preserve the common order. In Kotlin it is easily done thank to extension function. Maybe in Java could be done providing helpers functions that return and take objects that wraps curried functions and internally calls your new API. Example:
curry(ContactInfo:new)
   .apply(emailValidator.composable().validate())
   .apply(phoneValidator.composable().validate())

In my mind, this should lower the cognitive load of the user of the API.

Thank you for your great work 😄

@making
Copy link
Owner

making commented May 13, 2021

Although I would have been very happy to provide an integration of our libraries 😆.

If you are okay to keep it well maintained, I'll accept the pr that adds Konado as an optional dependency and the kotlin extension to Validator that returns KonadValidator as bellow

validator.konado().validate(...)

I'm also planning to deprecate validateToEither and introduce validator.either().validate(...) instead.

@making
Copy link
Owner

making commented May 13, 2021

Instead of the sh scripts to create applicative composition and curry, maybe you could consider using annotation processing. But maybe you already considered it... and decided that sh scripts are just simpler 😅

Yeah, I don't want users to pull the processor in the yavi jar which is useless in the user code.

I mean instead of being something like fnName arg1 arg2 ... argN it becomes arg1 arg2 ... argN fnName

If you are talking about apply(phoneNumberValidation, phoneNumberValidation, ContactInfo:new).

Actually, I considered apply(ContactInfo:new, phoneNumberValidation, phoneNumberValidation) first as vavr has the signature. but I found the former is easier to write a lambda function using the help of IDE code completion as the signature of the lambda is already fixed.

curry(ContactInfo:new)
   .apply(emailValidator.composable().validate())
   .apply(phoneValidator.composable().validate())

This looks an interesting approach. I'll give it a try.

making added a commit that referenced this issue May 13, 2021
@making
Copy link
Owner

making commented May 14, 2021

BTW, I have changed apply(v1, v2, v3, ..., f) to apply(f, v1, v2, v3, v4, ...)

making added a commit that referenced this issue May 14, 2021
This commit introduces an implementation similar to Vavr's Validation or Scalaz's Validation control class that is an applicative functor and facilitates accumulating errors.
closes gh-119
@making making changed the title Kotlin integration with Konad library Support Validation using Applicative Functor May 20, 2021
@making
Copy link
Owner

making commented May 20, 2021

YAVI 0.6.0 has been released though it's not yet synced with Maven Central
https://github.com/making/yavi/releases/tag/0.6.0

Final form looks like

Validator<Email> emailValidator = ...;
Validator<PhoneNumber> phoneNumberValidator = ...;

Validated<Email> emailValidated = emailValidator.applicative().validate(email);
Validated<PhoneNumber> phoneNumberValidated = phoneNumberValidator.applicative().validate(phoneNumber);

Validated<ContactInfo> contactInfoValidated = emailValidated.combine(phoneNumberValidated)
            .apply((em, ph) -> new ContactInfo(em, ph));
// or
Validated<ContactInfo> contactInfoValidated = Validations.combine(emailValidated, phoneNumberValidation)
		.apply((em, ph) -> new ContactInfo(em, ph));

boolean isValid = contactInfoValidated.isValid();

ContactInfo contactInfo = contactInfoValidated
        		.orElseThrow(violations -> new ConstraintViolationsException(violation));

HttpStatus status = contactInfoValidated
        		.fold(violations -> HttpStatus.BAD_REQUEST, contactInfo -> HttpStatus.OK);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants