Reputation: 770
public interface CustomerRegVali extends Function<Customer, ValidationResult>
static CustomerRegVali isEmailValid(){
return customer -> customer.getEmail().contains("@") ?
SUCCESS : EMAIL_NOT_VALID;
}
static CustomerRegVali isDobValid(){
return customer -> Period.between(customer.getDob(), LocalDate.now()).getYears() > 16 ?
SUCCESS : NOT_ADULT;
}
static CustomerRegVali isPhoneValid(){
return customer -> customer.getPhone().startsWith("+0") ?
SUCCESS : PHONE_NOT_VALID;
}
default CustomerRegVali and(CustomerRegVali other){
return customer -> {
ValidationResult result = CustomerRegVali.this.apply(customer);
return result.equals(SUCCESS) ? other.apply(customer) : result;
};
}
@Main method
ValidationResult result = isEmailValid()
.and(isPhoneValid())
.and(isDobValid())
.apply(customer);
Right so.. Looking back at an old uni project for functional java, I stumbled upon this combinator. Am I clueless or does .apply not get called twice on the primitives? Seems rather redundant.
Upvotes: 1
Views: 896
Reputation: 8204
You wrote
... during .and(isPhoneValid), is .this not referring to isEmailValid - and .other to isPhoneValid. With this in mind, next iteration would then be .this referring to isPhoneValid on which we will call .apply once again.
I think you're confusing this
with CustomerRegVali.this
, which is different. You might be imagining that you're building a structure that looks like this:
but in fact it looks like this:
Within a call to apply
, this
points to one of these boxes, but as you can see, there's never a case in which this
points to isEmailValid
while a separate other
link points to isPhoneValid
.
In the implementation of apply
for and
, you're not calling apply on this
but rather on the captured value CustomerRegVali.this
, which is the validator on which you invoked and
.
In setting up the validator, you:
isEmailValid
.and
on the isEmailValid
validator, passing the isPhoneValid
validator. This creates a new and
validator that retains a reference to the isEmailValid
validator and other
.and
on the result of the first and
, passing a new isDobValid
validator, building a new and
validator just like the previous one.That process produces the structure in the second diagram.
So there are five validators in total, and you call apply
once on each.
Upvotes: 1
Reputation: 9408
The three isXyzValid
values are of type CustomerRegVali
, meaning that they're functions that take a Customer
as an argument and return a ValidationResult
.
The and
function is a combinator that combines two validation functions into one. I.e. it returns a function that takes a customer as an argument and returns a ValidationResult according to whether the two validation functions succeed when applied to the given customer. It has a short-circuit so that if the first validation function fails then the second isn't called - the combined function simply returns the failed validation result returned by the first function.
The and
combinator is defined in the example code as a non-static method of the CustomerRegValin type. It could have been defined as a standalone static method, in which case it would take two function arguments (i.e.
CustomerRegVali` values) and would look like this:
CustomerRegVali and(CustomerRegVali thiz, CustomerRegVali other) {
return customer -> {
ValidationResult result = thiz.apply(customer);
return result.equals(SUCCESS) ? other.apply(customer) : result;
};
It should be clear here that apply is called for each function argument once (at most).
To use this method you would have to call it like this:
CustomerRegVali isEmailandDobValid = and(isEmailValid(), isDobValid());
If you wanted to chain another function using and
, then it would look like this:
CustomerRegVali isEmailandDobAndPhoneValid = and(and(isEmailValid(), isDobValid()), isPhoneValid());
It's evidently not that readable. We could improve this if we could use and
as an infix operator, e.g.:
isEmailValid() and isDobValid() and isPhoneValid()
Unfortunately Java doesn't support user-defined infix functions, however we can get fairly close by changing and
to be a non-static method on CustomerRegVali - this then allows us to write the above expression like this:
isEmailValid().and(isDobValid()).and(isPhoneValid())
To do this we need to change our and
definition so that it is a non-static method and also remove this first argument (as that function argument will now be the object on which we're calling the and
method). As a first attempt we might think this would work:
CustomerRegVali and(CustomerRegVali other) {
return customer -> {
ValidationResult result = this.apply(customer);
return result.equals(SUCCESS) ? other.apply(customer) : result;
};
however this will fail to compile. The problem is that inside of the lambda expression (the body of the function inside the inner pair of braces), this
refers to the immediate enclosing scope - the lambda itself and not to the outer CustomerRegVali
object. To fix this we need to disambigute this
by prefixing it with CustomerRegVali.
:
CustomerRegVali and(CustomerRegVali other) {
return customer -> {
ValidationResult result = CustomerRegVali.this.apply(customer);
return result.equals(SUCCESS) ? other.apply(customer) : result;
};
This now matches the definition provided in the example. We can use this and
method to chain 3 validation functions together:
CustomerRegVali isEmailandDobAndPhoneValid =
isEmailValid().and(isDobValid()).and(isPhoneValid())
and if we want to apply the function to an actual customer object then we need to call the apply
method:
ValidationResult result =
isEmailValid()
.and(isDobValid())
.and(isPhoneValid())
.apply(customer);
I hope it is now clear that each validation function is only called once (at most) in this example.
Upvotes: 0
Reputation: 53411
Please, be aware that you are building a chain of functions and the result of the chain is a function as well.
As @SilvioMayolo pointed out too, when you perform apply
on the chain this function in turn will invoke, in the order you indicated, if the validation result is SUCCESS
and the chain could proceed, the different, individual, CustomerRegVali
apply
methods.
In order to understand this behavior, consider the following interface definition.
import java.time.LocalDate;
import java.time.Period;
import java.util.function.Function;
public interface CustomerRegVali extends Function<Customer, ValidationResult> {
static CustomerRegVali isEmailValid(){
return customer -> {
System.out.println("isEmailValid called");
return customer.getEmail().contains("@") ?
SUCCESS : EMAIL_NOT_VALID;
};
}
static CustomerRegVali isDobValid(){
return customer -> {
System.out.println("isDobValid called");
return Period.between(customer.getDob(), LocalDate.now()).getYears() > 16 ?
SUCCESS : NOT_ADULT;
};
}
static CustomerRegVali isPhoneValid(){
return customer -> {
System.out.println("isPhoneValid called");
return customer.getPhone().startsWith("+0") ?
SUCCESS : PHONE_NOT_VALID;
};
}
default CustomerRegVali and(CustomerRegVali other){
return customer -> {
System.out.println("and called");
ValidationResult result = CustomerRegVali.this.apply(customer);
return result.equals(SUCCESS) ? other.apply(customer) : result;
};
}
}
And this simple test case:
public static void main(String... args) {
Customer customer = new Customer();
customer.setEmail("[email protected]");
customer.setDob(LocalDate.of(2000, Month.JANUARY, 1));
customer.setPhone("+000000000");
final CustomerRegVali chain = isEmailValid()
.and(isPhoneValid())
.and(isDobValid());
System.out.println("valid result:");
ValidationResult result = chain.apply(customer);
System.out.println(result);
customer.setPhone("+900000000");
System.out.println("\ninvalid result:");
System.out.println(chain.apply(customer));
}
This will output:
valid result:
and called
and called
isEmailValid called
isPhoneValid called
isDobValid called
SUCCESS
invalid result:
and called
and called
isEmailValid called
isPhoneValid called
PHONE_NOT_VALID
As you can see, as a consequence of the function chaining and the way you programmed these functions, the individual apply
methods will be called when you invoke the final chain's apply
method in the order specified and only if the chain should proceed because the validation result is SUCCESS
.
Precisely, assuming your code:
@Main method
ValidationResult result = isEmailValid()
.and(isPhoneValid())
.and(isDobValid())
.apply(customer);
apply
you are invoking the second and
function.and
function, this will in turn invoke the apply
method of the function in which the second and
is defined. In this case, it implies that the apply
method of the first defined and
function will be invoked.and
function, the apply
method of the function in which the first and
function is defined will be invoked, which happens to be isEmailValid
now.isEmailValid
is invoked. If it returns SUCCESS
then, according to the logic implemented in the and
function - we are moving forward now, from the first validator to the last one - it will invoke the next validator function in the chain, the one provided as argument of the first and
function, isPhoneValid
, in this case.isPhoneValid
is invoked. If it returns SUCCESS
then, according to the logic implemented in the and
function, again it will invoke the next validator function in the chain, the one passed as argument of the second and
function this time, isDobValid
in this case.isDobValid
is invoked. The process would continue like this if new validators exist.Closely related, some time ago I came across this wonderful article about functional programming and partial
functions. I think it can be of help.
Upvotes: 1