No Java 8, é estilisticamente melhor usar expressões de referência de método ou métodos retornando uma implementação da interface funcional?


11

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?


1
Os lambdas foram adicionados à linguagem por um motivo e não para piorar o Java.

@ Snowman Isso escusado será dizer. Portanto, presumo que exista alguma relação implícita entre seu comentário e minha pergunta que não estou entendendo direito. Qual é o ponto que você está tentando enfatizar?
M. Justin

2
Suponho que o argumento que ele está afirmando é que, se os lambdas não tivessem sido uma melhoria no status quo, eles não teriam se incomodado em apresentá-los.
9788 Robert

2
@RobertHarvey Sure. Nesse ponto, no entanto, as referências de lambdas e de método foram adicionadas ao mesmo tempo, assim como o status quo antes. Mesmo sem esse caso específico, há momentos em que as referências ao método ainda seriam melhores; por exemplo, um método público existente (por exemplo 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.
23716 Justin

Respostas:


8

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:

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

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:

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

Agora, suponha que tenhamos um método Result deriveResult(Foo foo). Como um Predicado é uma Função, podemos construir um novo predicado que primeiro chama deriveResulte depois testa o resultado derivado:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

Embora a implementação de composeuse estilo pontual com lambdas, o uso de composição para definir isFoosResultInFutureseja 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.


1
Eu acho que o que meu colega e eu estamos discutindo são mais essas duas atribuições: Predicate<Result> pred = this::isResultInFuture;e Predicate<Result> pred = resultInFuture();onde resultInFuture () retorna a Predicate<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.
M. Justin

4
@ M.Justin: A resultInFuture();tarefa é idêntica à tarefa lambda que apresentei, exceto no meu caso, seu resultInFuture()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!
Jack

5

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


As operações de ordem superior no predicado ainda estão disponíveis, lançando a referência do método:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack

4

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:

people = sort(people, (a,b) -> b.age() - a.age());

Compare isso com a legibilidade ao tentar alinhar seu exemplo lambda:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

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.


2
Re: verbosidade - Embora as novas adições funcionais não reduzam por si só muita verbosidade, elas permitem maneiras de fazê-lo. Por exemplo, você pode definir: 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.
JimmyJames

Esse é um ótimo exemplo do que eu estava falando, @JimmyJames. Quando as lambdas são curtas e podem ser incorporadas, elas são uma grande melhoria. Quando eles demoram tanto que você deseja separá-los e dar um nome a eles, é melhor definir um método. Obrigado por me ajudar a entender como minha resposta foi mal interpretada.
Karl Bielefeldt

Eu acho que sua resposta foi ótima. Não sei por que seria votado para baixo.
precisa saber é o seguinte
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.