«1. Введение

Начиная с Java 8, мы можем определять функции с одним и двумя параметрами в Java, что позволяет нам внедрять их поведение в другие функции, передавая их в качестве параметров. Но для функций с большим количеством параметров мы полагаемся на внешние библиотеки, такие как Vavr.

Другой вариант — использовать каррирование. Комбинируя каррирование и функциональные интерфейсы, мы можем даже определить легко читаемые компоновщики, которые заставят пользователя вводить все входные данные.

В этом уроке мы дадим определение каррированию и представим его использование.

2. Простой пример

Рассмотрим конкретный пример письма с несколькими параметрами.

Наша упрощенная первая версия нуждается только в теле и приветствии:

class Letter {
    private String salutation;
    private String body;
    
    Letter(String salutation, String body){
        this.salutation = salutation;
        this.body = body;
    }
}

2.1. Создание по методу

Такой объект можно легко создать с помощью метода:

Letter createLetter(String salutation, String body){
    return new Letter(salutation, body);
}

2.2. Создание с помощью BiFunction

Описанный выше метод прекрасно работает, но нам может понадобиться обеспечить такое поведение для чего-то, написанного в функциональном стиле. Начиная с Java 8, мы можем использовать BiFunction для этой цели:

BiFunction<String, String, Letter> SIMPLE_LETTER_CREATOR 
  = (salutation, body) -> new Letter(salutation, body);

2.3. Создание с помощью последовательности функций

Мы также можем переформулировать это как последовательность функций, каждая из которых имеет один параметр:

Function<String, Function<String, Letter>> SIMPLE_CURRIED_LETTER_CREATOR 
  = salutation -> body -> new Letter(salutation, body);

Мы видим, что приветствие отображается в функцию. Полученная функция сопоставляется с новым объектом Letter. Посмотрите, как изменился возвращаемый тип по сравнению с BiFunction. Мы используем только класс Function. Такое преобразование в последовательность функций называется каррированием.

3. Расширенный пример

Чтобы показать преимущества каррирования, давайте расширим наш конструктор класса Letter дополнительными параметрами:

class Letter {
    private String returningAddress;
    private String insideAddress;
    private LocalDate dateOfLetter;
    private String salutation;
    private String body;
    private String closing;

    Letter(String returningAddress, String insideAddress, LocalDate dateOfLetter, 
      String salutation, String body, String closing) {
        this.returningAddress = returningAddress;
        this.insideAddress = insideAddress;
        this.dateOfLetter = dateOfLetter;
        this.salutation = salutation;
        this.body = body;
        this.closing = closing;
    }
}

3.1. Создание по методу

Как и раньше, мы можем создавать объекты с помощью метода:

Letter createLetter(String returnAddress, String insideAddress, LocalDate dateOfLetter, 
  String salutation, String body, String closing) {
    return new Letter(returnAddress, insideAddress, dateOfLetter, salutation, body, closing);
}

3.2. Функции для произвольной Arity

Arbitrary Arity — это мера количества параметров, которые принимает функция. Java предоставляет существующие функциональные интерфейсы для nullary (Supplier), унарных (Function) и бинарных (BiFunction), но это все. Без определения нового функционального интерфейса мы не можем предоставить функцию с шестью входными параметрами.

Каррирование — наш выход. Он преобразует произвольную арность в последовательность унарных функций. Итак, для нашего примера получаем:

Function<String, Function<String, Function<LocalDate, Function<String,
  Function<String, Function<String, Letter>>>>>> LETTER_CREATOR =
  returnAddress
    -> closing
    -> dateOfLetter
    -> insideAddress
    -> salutation
    -> body
    -> new Letter(returnAddress, insideAddress, dateOfLetter, salutation, body, closing);

3.3. Подробный тип

Очевидно, что приведенный выше тип не совсем читабелен. В этой форме мы используем «применить» шесть раз, чтобы создать письмо:

LETTER_CREATOR
  .apply(RETURNING_ADDRESS)
  .apply(CLOSING)
  .apply(DATE_OF_LETTER)
  .apply(INSIDE_ADDRESS)
  .apply(SALUTATION)
  .apply(BODY);

3.4. Предварительное заполнение значений

С помощью этой цепочки функций мы можем создать помощника, который предварительно заполняет первые значения и возвращает функцию для дальнейшего завершения объекта письма:

Function<String, Function<LocalDate, Function<String, Function<String, Function<String, Letter>>>>> 
  LETTER_CREATOR_PREFILLED = returningAddress -> LETTER_CREATOR.apply(returningAddress).apply(CLOSING);

Обратите внимание, что для этого быть полезным, мы должны тщательно выбирать порядок параметров в исходной функции, чтобы менее специфичные были первыми.

4. Шаблон построителя

Чтобы преодолеть недружественное определение типа и многократное использование стандартного метода применения, что означает, что вы не имеете понятия о правильном порядке ввода, мы можем использовать шаблон построителя:

AddReturnAddress builder(){
    return returnAddress
      -> closing
      -> dateOfLetter
      -> insideAddress
      -> salutation
      -> body
      -> new Letter(returnAddress, insideAddress, dateOfLetter, salutation, body, closing);
}

~~ ~ Вместо последовательности функций мы используем последовательность функциональных интерфейсов. Обратите внимание, что возвращаемый тип приведенного выше определения — AddReturnAddress. В дальнейшем нам нужно только определить промежуточные интерфейсы:

interface AddReturnAddress {
    Letter.AddClosing withReturnAddress(String returnAddress);
}
    
interface AddClosing {
    Letter.AddDateOfLetter withClosing(String closing);
}
    
interface AddDateOfLetter {
    Letter.AddInsideAddress withDateOfLetter(LocalDate dateOfLetter);
}

interface AddInsideAddress {
    Letter.AddSalutation withInsideAddress(String insideAddress);
}

interface AddSalutation {
    Letter.AddBody withSalutation(String salutation);
}

interface AddBody {
    Letter withBody(String body);
}

Таким образом, использование этого для создания письма не требует пояснений:

Letter.builder()
  .withReturnAddress(RETURNING_ADDRESS)
  .withClosing(CLOSING)
  .withDateOfLetter(DATE_OF_LETTER)
  .withInsideAddress(INSIDE_ADDRESS)
  .withSalutation(SALUTATION)
  .withBody(BODY));

Как и раньше, мы можем предварительно заполнить объект письма: ~ ~~

AddDateOfLetter prefilledLetter = Letter.builder().
  withReturnAddress(RETURNING_ADDRESS).withClosing(CLOSING);

Обратите внимание, что интерфейсы обеспечивают порядок заполнения. Таким образом, мы не можем просто предварительно заполнить закрытие.

5. Заключение

Мы увидели, как применять каррирование, поэтому мы не ограничены ограниченным числом параметров, поддерживаемых стандартными функциональными интерфейсами Java. Кроме того, мы можем легко предварительно заполнить первые несколько параметров. Кроме того, мы узнали, как использовать это для создания удобочитаемого компоновщика.

Как всегда, полные примеры кода доступны на GitHub.