O Java 8 adicionou o conceito de interfaces funcionais , bem como vários novos métodos projetados para assumir interfaces funcionais. Instâncias dessas interfaces podem ser criadas de forma sucinta usando expressões de referência de método (por exemplo SomeClass::someMethod
) e expressões lambda (por exemplo (x, y) -> x + y
).
Um colega e eu temos opiniões divergentes sobre quando é melhor usar um formulário ou outro (onde "melhor", neste caso, realmente se resume a "mais legível" e "mais alinhado às práticas padrão", pois é basicamente o contrário. equivalente). Especificamente, isso envolve o caso em que tudo a seguir é verdadeiro:
- a função em questão não é usada fora de um único escopo
- dar um nome à instância ajuda na legibilidade (em vez de, por exemplo, a lógica ser simples o suficiente para ver o que está acontecendo rapidamente)
- não há outras razões de programação pelas quais um formulário seja preferível ao outro.
Minha opinião atual sobre o assunto é que adicionar um método privado e referir isso por referência de método é a melhor abordagem. Parece que foi assim que o recurso foi projetado para ser usado, e parece mais fácil comunicar o que está acontecendo por meio de nomes e assinaturas de métodos (por exemplo, "booleano isResultInFuture (resultado do resultado)" está claramente dizendo que está retornando um booleano). Ele também torna o método privado mais reutilizável se um aprimoramento futuro da classe quiser fazer uso da mesma verificação, mas não precisar do wrapper da interface funcional.
A preferência do meu colega é ter um método que retorne a instância da interface (por exemplo, "Predicate resultInFuture ()"). Para mim, parece que não é exatamente como o recurso deveria ser usado, parece um pouco mais desajeitado e parece que é mais difícil realmente comunicar a intenção por meio de nomes.
Para tornar esse exemplo concreto, aqui está o mesmo código, escrito nos diferentes estilos:
public class ResultProcessor {
public void doSomethingImportant(List<Result> results) {
results.filter(this::isResultInFuture).forEach({ result ->
// Do something important with each future result line
});
}
private boolean isResultInFuture(Result result) {
someOtherService.getResultDateFromDatabase(result).after(new Date());
}
}
vs.
public class ResultProcessor {
public void doSomethingImportant(List<Result> results) {
results.filter(resultInFuture()).forEach({ result ->
// Do something important with each future result line
});
}
private Predicate<Result> resultInFuture() {
return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
}
}
vs.
public class ResultProcessor {
public void doSomethingImportant(List<Result> results) {
Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
results.filter(resultInFuture).forEach({ result ->
// Do something important with each future result line
});
}
}
Existe alguma documentação ou comentário oficial ou semioficial sobre se uma abordagem é mais preferida, mais alinhada com as intenções dos designers de idiomas ou mais legível? Salvo uma fonte oficial, existem razões claras pelas quais alguém seria a melhor abordagem?
Object::toString
). Portanto, minha pergunta é mais sobre se é melhor nesse tipo específico de instância que estou apresentando aqui do que se existem casos em que um é melhor que o outro ou vice-versa.Respostas:
Em termos de programação funcional, o que você e seu colega estão discutindo é o estilo sem pontos , mais especificamente a redução de eta . Considere as duas atribuições a seguir:
Eles são operacionalmente equivalentes, e o primeiro é chamado estilo pontual , enquanto o segundo é estilo sem pontos . O termo "ponto" refere-se a um argumento de função nomeado (isso vem da topologia) que está ausente no último caso.
De maneira mais geral, para todas as funções F, um wrapper lambda que pega todos os argumentos fornecidos e os passa para F inalterados e retorna o resultado de F inalterado é idêntico ao próprio F. Remover o lambda e usar F diretamente (passando do estilo pontual para o estilo livre de pontos acima) é chamado de redução eta , um termo que deriva do cálculo lambda .
Existem expressões sem pontos que não são criadas pela redução eta, o exemplo mais clássico dos quais é a composição de funções . Dada uma função do tipo A ao tipo B e uma função do tipo B ao tipo C, podemos compor essas funções em uma função do tipo A ao tipo C:
Agora, suponha que tenhamos um método
Result deriveResult(Foo foo)
. Como um Predicado é uma Função, podemos construir um novo predicado que primeiro chamaderiveResult
e depois testa o resultado derivado:Embora a implementação de
compose
use estilo pontual com lambdas, o uso de composição para definirisFoosResultInFuture
seja isento de pontos, pois nenhum argumento precisa ser mencionado.A programação sem pontos também é chamada de programação tácita e pode ser uma ferramenta poderosa para aumentar a legibilidade, removendo detalhes sem sentido (trocadilhos) de uma definição de função. O Java 8 não suporta programação livre de pontos quase tão minuciosamente quanto as linguagens mais funcionais como Haskell, mas uma boa regra é sempre executar a redução de eta. Não há necessidade de usar lambdas que não tenham um comportamento diferente das funções que envolvem.
fonte
Predicate<Result> pred = this::isResultInFuture;
ePredicate<Result> pred = resultInFuture();
onde resultInFuture () retorna aPredicate<Result>
. Portanto, embora essa seja uma ótima explicação de quando usar a referência de método versus uma expressão lambda, ela não cobre totalmente esse terceiro caso.resultInFuture();
tarefa é idêntica à tarefa lambda que apresentei, exceto no meu caso, seuresultInFuture()
método está embutido. Portanto, essa abordagem tem duas camadas de indireção (nenhuma das quais faz nada) - o método de construção de lambda e o próprio lambda. Pior ainda!Nenhuma das respostas atuais realmente aborda o núcleo da pergunta, que é se a classe deve ter um
private boolean isResultInFuture(Result result)
método ou umprivate Predicate<Result> resultInFuture()
método. Embora eles sejam amplamente equivalentes, existem vantagens e desvantagens para cada um.A primeira abordagem é mais natural se você chamar o método diretamente, além de criar uma referência de método. Por exemplo, se você testar este método, pode ser mais fácil usar a primeira abordagem. Outra vantagem da primeira abordagem é que o método não precisa se preocupar com a forma como é usado, separando-o do site de chamada. Se você alterar posteriormente a classe para chamar o método diretamente, esse desacoplamento será recompensado de maneira muito pequena.
A primeira abordagem também é superior se você desejar adaptar esse método a diferentes interfaces funcionais ou diferentes uniões de interfaces. Por exemplo, você pode querer que a referência do método seja do tipo
Predicate<Result> & Serializable
, que a segunda abordagem não oferece flexibilidade para alcançar.Por outro lado, a segunda abordagem é mais natural se você pretende executar operações de ordem superior no predicado. O Predicate possui vários métodos que não podem ser chamados diretamente em uma referência de método. Se você pretende usar esses métodos, a segunda abordagem é superior.
Em última análise, a decisão é subjetiva. Mas se você não pretende chamar métodos no Predicate, gostaria de preferir a primeira abordagem.
fonte
((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Não escrevo mais código Java, mas escrevo em uma linguagem funcional para viver, e também apoio outras equipes que estão aprendendo programação funcional. As lambdas têm um ponto delicado de legibilidade. O estilo geralmente aceito para eles é que você os usa quando pode incorporá-los, mas se sentir a necessidade de atribuí-lo a uma variável para uso posterior, provavelmente deverá definir apenas um método.
Sim, as pessoas atribuem lambdas a variáveis nos tutoriais o tempo todo. Esses são apenas tutoriais para ajudar a fornecer uma compreensão completa de como eles funcionam. Na verdade, eles não são usados dessa maneira na prática, exceto em circunstâncias relativamente raras (como escolher entre duas lambdas usando uma expressão if).
Isso significa que, se o seu lambda for curto o suficiente para ser facilmente legível após a inclusão, como no exemplo do comentário de @JimmyJames, então siga em frente:
Compare isso com a legibilidade ao tentar alinhar seu exemplo lambda:
Para o seu exemplo em particular, eu o sinalizaria pessoalmente em uma solicitação de recebimento e solicitaria que você o retirasse para um método nomeado devido ao seu comprimento. Java é uma linguagem irritantemente detalhada, portanto, talvez com o tempo suas próprias práticas específicas da linguagem sejam diferentes de outras linguagens, mas até então, mantenha suas lambdas curtas e alinhadas.
fonte
public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)
com a implementação óbvia e, em seguida, você pode escrever um código como este:people = sort(people, (a,b) -> b.age() - a.age());
que é uma grande melhoria na IMO.