组合器模式,java 函数 <e, t>

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);

是这样.. 回顾一个旧的函数式 uni 项目 java,我偶然发现了这个组合器。我是一无所知还是 .apply 没有在基元上被调用两次?显得有些多余。

请注意,您正在构建一个函数链,链的结果也是一个函数。

正如@SilvioMayolo 也指出的那样,当您在链上执行 apply 时,如果验证结果为 SUCCESS 且链可以,则此函数将按照您指定的顺序依次调用继续,不同的,个人的,CustomerRegVali apply 方法。

为了理解这种行为,请考虑以下接口定义。

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;
      };
    }
}

这个简单的测试用例:

public static void main(String... args) {
  Customer customer = new Customer();
  customer.setEmail("email@company.org");
  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));
}

这将输出:

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

如您所见,由于函数链和您对这些函数的编程方式,当您在指定顺序并且仅当链应该继续时因为验证结果是 SUCCESS.

准确地说,假设您的代码:

@Main method
    ValidationResult result = isEmailValid()
            .and(isPhoneValid())
            .and(isDobValid())
            .apply(customer);
  • 当您调用 apply 时,您正在调用第二个 and 函数。
  • 根据为 and 函数提供的实现,这将依次调用定义第二个 and 的函数的 apply 方法。在这种情况下,这意味着将调用第一个定义的 and 函数的 apply 方法。
  • 反过来,再次根据为and函数提供的实现,将调用定义第一个and函数的函数的apply方法,这现在刚好是isEmailValid
  • isEmailValid 被调用。如果它 returns SUCCESS 那么,根据 and 函数中实现的逻辑——我们现在向前推进,从第一个验证器到最后一个——它将调用下一个验证器函数在链中,作为第一个 and 函数的参数提供的那个,isPhoneValid,在这种情况下。
  • isPhoneValid 被调用。如果它 returns SUCCESS 那么,根据 and 函数中实现的逻辑,它将再次调用链中的下一个验证器函数,该函数作为第二个 [=] 的参数传递这次是 22=] 函数,在本例中是 isDobValid
  • isDobValid 被调用。如果存在新的验证器,该过程将像这样继续。

密切相关,前段时间我遇到了 this wonderful article 关于函数式编程和 partial 函数。我认为它可以提供帮助。

你写了

... 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.

我认为您混淆了 thisCustomerRegVali.this,这是不同的。您可能会想象您正在构建一个如下所示的结构:

但实际上是这样的:

在对 apply 的调用中,this 指向这些框之一,但如您所见,从来没有 this 指向 isEmailValid 而单独的 other link 指向 isPhoneValid.

andapply 实现中,您不是在 this 上调用应用程序,而是在捕获的值 CustomerRegVali.this 上调用应用程序,它是你调用了 and.

在设置验证器时,您:

  • 呼叫isEmailValid.
  • isEmailValid 验证器上调用 and,传递 isPhoneValid 验证器。这将创建一个新的 and 验证器,它保留对 isEmailValid 验证器和 other.
  • 的引用
  • 最后在第一个 and 的结果上调用 and,传递一个新的 isDobValid 验证器,构建一个新的 and 验证器,就像之前的验证器一样。

该过程生成第二张图中的结构。

所以总共有五个验证器,你对每个调用一次 apply

这三个 isXyzValid 值的类型是 CustomerRegVali,这意味着它们是以 Customer 作为参数并且 return 和 ValidationResult.

and 函数是一个将两个验证函数合二为一的组合器。 IE。它 return 是一个将客户作为参数的函数,return 是一个 ValidationResult,根据两个验证函数应用于给定客户时是否成功。它有一个短路,因此如果第一个验证函数失败,则不会调用第二个 - 组合函数只是 returns 失败的验证结果 return 由第一个函数编辑。

and 组合子在示例代码中被定义为 CustomerRegValin type. It could have been defined as a standalone static method, in which case it would take two function arguments (i.e. CustomerRegVali` 值的非静态方法)并且看起来像这样:

CustomerRegVali and(CustomerRegVali thiz, CustomerRegVali other) {
    return customer -> {
        ValidationResult result = thiz.apply(customer);
        return result.equals(SUCCESS) ? other.apply(customer) : result;
    };

这里应该清楚,每个函数参数调用apply一次(最多)。

要使用此方法,您必须这样调用它:

CustomerRegVali isEmailandDobValid = and(isEmailValid(), isDobValid());

如果您想使用 and 链接另一个函数,那么它将如下所示:

CustomerRegVali isEmailandDobAndPhoneValid = and(and(isEmailValid(), isDobValid()), isPhoneValid());

显然可读性不强。如果我们可以使用 and 作为中缀运算符,我们可以改进这一点,例如:

isEmailValid() and isDobValid() and isPhoneValid()

不幸的是 Java 不支持用户定义的中缀函数,但是我们可以通过将 and 更改为 CustomerRegVali 上的非静态方法来获得相当接近的结果 - 这样我们就可以编写上面的表达式是这样的:

isEmailValid().and(isDobValid()).and(isPhoneValid())

为此,我们需要更改 and 定义,使其成为非静态方法,并删除第一个参数(因为该函数参数现在将成为我们调用的对象and 方法)。作为第一次尝试,我们可能认为这会奏效:

CustomerRegVali and(CustomerRegVali other) {
    return customer -> {
        ValidationResult result = this.apply(customer);
        return result.equals(SUCCESS) ? other.apply(customer) : result;
    };

然而这将无法编译。问题在于,在 lambda 表达式内部(内部一对大括号内的函数体),this 指的是直接封闭范围 - lambda 本身,而不是外部 CustomerRegVali 对象。为了解决这个问题,我们需要通过在 this 前面加上 CustomerRegVali.:

来消除歧义
CustomerRegVali and(CustomerRegVali other) {
    return customer -> {
        ValidationResult result = CustomerRegVali.this.apply(customer);
        return result.equals(SUCCESS) ? other.apply(customer) : result;
    };

这现在与示例中提供的定义匹配。我们可以使用这个 and 方法将 3 个验证函数链接在一起:

CustomerRegVali isEmailandDobAndPhoneValid =
        isEmailValid().and(isDobValid()).and(isPhoneValid())

如果我们想将该函数应用于实际的客户对象,那么我们需要调用 apply 方法:

ValidationResult result =
        isEmailValid()
                .and(isDobValid())
                .and(isPhoneValid())
                .apply(customer);

我希望现在可以清楚地知道在这个例子中每个验证函数只调用一次(最多)。