combinator pattern, java Function <e, t>

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

Answers (3)

Willis Blackburn
Willis Blackburn

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:

enter image description here

but in fact it looks like this:

enter image description here

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:

  • Call isEmailValid.
  • Invoke 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.
  • Finally invoke 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

jon hanson
jon hanson

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

jccampanero
jccampanero

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);
  • When you invoke apply you are invoking the second and function.
  • According to the implementation provided for the 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.
  • In turn, again according to the implementation provided for the 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

Related Questions