Caril em Java
1. Introdução
Desde o Java 8, podemos definir funções de um e dois parâmetros em Java, permitindo injetar seus comportamentos em outras funções, passando-os como parâmetros. Mas para funções com mais parâmetros, contamos com bibliotecas externas comoVavr.
Outra opção é usarcurrying. Ao combinar currying efunctional interfaces, podemos até definir construtores fáceis de ler que forçam o usuário a fornecer todas as entradas.
In this tutorial, we’ll define currying and present its usage.
2. Exemplo Simples
Vamos considerar um exemplo concreto de uma carta com vários parâmetros.
Nossa primeira versão simplificada precisa apenas de um corpo e uma saudação:
class Letter {
private String salutation;
private String body;
Letter(String salutation, String body){
this.salutation = salutation;
this.body = body;
}
}
2.1. Criação por método
Esse objeto pode ser facilmente criado com um método:
Letter createLetter(String salutation, String body){
return new Letter(salutation, body);
}
2.2. Criação com umBiFunction
O método acima funciona muito bem, mas talvez seja necessário fornecer esse comportamento a algo escrito no estilo funcional. Desde o Java 8, podemos usarBiFunction para este propósito:
BiFunction SIMPLE_LETTER_CREATOR
= (salutation, body) -> new Letter(salutation, body);
2.3. Criação com uma sequência de funções
Também podemos reafirmar isso como uma sequência de funções, cada uma com um parâmetro:
Function> SIMPLE_CURRIED_LETTER_CREATOR
= salutation -> body -> new Letter(salutation, body);
Vemos quesalutation mapeia para uma função. A função resultante é mapeada para o novo objetoLetter. Veja como o tipo de retorno mudou deBiFunction. Estamos usando apenas a classeFunction. Such a transformation to a sequence of functions is called currying.
3. Exemplo Avançado
Para mostrar as vantagens do currying, vamos estender nosso construtor de classeLetter com mais parâmetros:
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. Criação por método
Como antes, podemos criar objetos com um método:
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. Funções para Arity Arbitrary
Arity é uma medida do número de parâmetros que uma função usa. Java fornece interfaces funcionais existentes paranullary (Supplier),unary (Function) e binário (BiFunction), mas é isso. Sem definir uma nova interface funcional, não podemos fornecer uma função com seis parâmetros de entrada.
Currying é a nossa saída. It transforms an arbitrary arity into a sequence of unary functions. Então, para o nosso exemplo, obtemos:
Function>>>>> LETTER_CREATOR =
returnAddress
-> closing
-> dateOfLetter
-> insideAddress
-> salutation
-> body
-> new Letter(returnAddress, insideAddress, dateOfLetter, salutation, body, closing);
3.3. Tipo verboso
Obviamente, o tipo acima não é muito legível. Com este formulário, usamos‘apply' seis vezes para criar umLetter:
LETTER_CREATOR
.apply(RETURNING_ADDRESS)
.apply(CLOSING)
.apply(DATE_OF_LETTER)
.apply(INSIDE_ADDRESS)
.apply(SALUTATION)
.apply(BODY);
3.4. Valores de pré-preenchimento
Com essa cadeia de funções, podemos criar um auxiliar que preenche previamente os primeiros valores e retorna a função para a conclusão posterior do objeto de letra:
Function>>>>
LETTER_CREATOR_PREFILLED = returningAddress -> LETTER_CREATOR.apply(returningAddress).apply(CLOSING);
Observe que, para ser útil,we have to carefully choose the order of the parameters in the original function so that the less specific are the first ones.
4. Padrão do Construtor
Para superar a definição de tipo hostil e o uso repetido do métodoapply padrão, o que significa que você não tem pistas sobre a ordem correta das entradas, podemos usar obuilder pattern:
AddReturnAddress builder(){
return returnAddress
-> closing
-> dateOfLetter
-> insideAddress
-> salutation
-> body
-> new Letter(returnAddress, insideAddress, dateOfLetter, salutation, body, closing);
}
Instead of a sequence of functions, we use a sequence of functional interfaces. Observe que o tipo de retorno da definição acima éAddReturnAddress. A seguir, temos apenas que definir as interfaces intermediárias:
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);
}
Portanto, usar isso para criar umLetter é bastante autoexplicativo: __
Letter.builder()
.withReturnAddress(RETURNING_ADDRESS)
.withClosing(CLOSING)
.withDateOfLetter(DATE_OF_LETTER)
.withInsideAddress(INSIDE_ADDRESS)
.withSalutation(SALUTATION)
.withBody(BODY));
Como antes, podemos pré-preencher o objeto da letra:
AddDateOfLetter prefilledLetter = Letter.builder().
withReturnAddress(RETURNING_ADDRESS).withClosing(CLOSING);
Observe quethe interfaces ensure the filling order. Portanto, não podemos apenas pré-preencherclosing.
5. Conclusão
Vimos como aplicar currying, portanto, não somos limitados pelo número limitado de parâmetros suportados pelas interfaces funcionais Java padrão. Além disso, podemos facilmente preencher previamente os primeiros parâmetros. Além disso, aprendemos como usar isso para criar um construtor legível.
Como sempre, as amostras de código completas sãoavailable over on GitHub.