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 — это мера количества параметров, которые принимает функция. 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 .