En Java 8, est-il préférable d'un point de vue stylistique d'utiliser des expressions de référence de méthode ou des méthodes renvoyant une implémentation de l'interface fonctionnelle?

11

Java 8 a ajouté le concept d' interfaces fonctionnelles , ainsi que de nombreuses nouvelles méthodes conçues pour prendre des interfaces fonctionnelles. Les instances de ces interfaces peuvent être créées succinctement à l'aide d' expressions de référence de méthode (par exemple SomeClass::someMethod) et d' expressions lambda (par exemple (x, y) -> x + y).

Un collègue et moi avons des opinions divergentes sur le moment où il est préférable d'utiliser une forme ou une autre (où "meilleur" dans ce cas se résume vraiment à "le plus lisible" et "le plus conforme aux pratiques standard", car ils sont fondamentalement autrement équivalent). Plus précisément, cela implique le cas où les conditions suivantes sont toutes vraies:

  • la fonction en question n'est pas utilisée en dehors d'une seule portée
  • donner un nom à l'instance aide à la lisibilité (contrairement à par exemple la logique étant assez simple pour voir ce qui se passe en un coup d'œil)
  • il n'y a pas d'autres raisons de programmation pour lesquelles un formulaire serait préférable à l'autre.

Mon opinion actuelle à ce sujet est que l'ajout d'une méthode privée et la référence par référence à la méthode est la meilleure approche. Il semble que c'est ainsi que la fonctionnalité a été conçue pour être utilisée, et il semble plus facile de communiquer ce qui se passe via les noms et les signatures de méthode (par exemple, "boolean isResultInFuture (Result result)" dit clairement qu'il renvoie un booléen). Il rend également la méthode privée plus réutilisable si une future amélioration de la classe veut utiliser la même vérification, mais n'a pas besoin du wrapper d'interface fonctionnelle.

La préférence de mon collègue est d'avoir une méthode qui retourne l'instance de l'interface (par exemple "Predicate resultInFuture ()"). Pour moi, cela semble que ce n'est pas tout à fait la façon dont la fonctionnalité était destinée à être utilisée, se sent un peu plus maladroite et semble qu'il est plus difficile de vraiment communiquer l'intention par le biais de la dénomination.

Pour concrétiser cet exemple, voici le même code, écrit dans les différents styles:

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());
  }
}

contre.

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());
  }
}

contre.

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-t-il une documentation ou des commentaires officiels ou semi-officiels sur le point de savoir si une approche est plus préférée, plus conforme aux intentions des concepteurs de langage ou plus lisible? À moins d'une source officielle, y a-t-il des raisons claires pour lesquelles on serait la meilleure approche?

M. Justin
la source
1
Les lambdas ont été ajoutés au langage pour une raison, et ce n'était pas pour aggraver Java.
@Snowman Cela va sans dire. Je suppose donc qu'il y a une relation implicite entre votre commentaire et ma question que je ne comprends pas tout à fait. Quel est le point que vous essayez de faire valoir?
M. Justin
2
Je suppose que l'argument qu'il fait valoir est que, si les lambdas n'avaient pas amélioré le statu quo, ils n'auraient pas pris la peine de les présenter.
Robert Harvey
2
@RobertHarvey Bien sûr. À ce stade, cependant, les deux lambdas et les références de méthode ont été ajoutées en même temps, donc ni le statu quo auparavant. Même sans ce cas particulier, il y a des moments où les références de méthode seraient encore meilleures; par exemple, une méthode publique existante (par exemple Object::toString). Donc ma question est plus de savoir si c'est mieux dans ce type particulier d'instance que je présente ici, que s'il existe des cas où l'un est meilleur que l'autre, ou vice versa.
M. Justin

Réponses:

8

En termes de programmation fonctionnelle, ce que vous et votre collègue discutez est le style sans point , plus précisément la réduction eta . Considérez les deux affectations suivantes:

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

Ceux-ci sont équivalents sur le plan opérationnel, et le premier est appelé style pointu tandis que le second est un style sans point . Le terme "point" fait référence à un argument de fonction nommée (cela vient de la topologie) qui manque dans ce dernier cas.

Plus généralement, pour toutes les fonctions F, un wrapper lambda qui prend tous les arguments donnés et les passe à F inchangés, puis retourne le résultat de F inchangé est identique à F lui-même. Supprimer le lambda et utiliser directement F (en passant du style pointu au style sans point ci-dessus) est appelé une réduction eta , un terme qui découle du calcul lambda .

Il existe des expressions libres de points qui ne sont pas créées par la réduction eta, dont l'exemple le plus classique est la composition des fonctions . Étant donné une fonction du type A au type B et une fonction du type B au type C, nous pouvons les composer ensemble dans une fonction du type A au type C:

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

Supposons maintenant que nous ayons une méthode Result deriveResult(Foo foo). Puisqu'un prédicat est une fonction, nous pouvons construire un nouveau prédicat qui appelle d'abord deriveResultpuis teste le résultat dérivé:

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

Bien que la mise en œuvre des composeutilisations de style pointful avec lambdas, l' utilisation de Compose pour définir isFoosResultInFutureest le point libre , car aucun argument doivent être mentionnés.

La programmation sans point est également appelée programmation tacite , et elle peut être un outil puissant pour augmenter la lisibilité en supprimant les détails inutiles (jeu de mots) d'une définition de fonction. Java 8 ne prend pas en charge la programmation sans points presque aussi complètement que les langages plus fonctionnels comme Haskell, mais une bonne règle de base est de toujours effectuer une réduction eta. Il n'est pas nécessaire d'utiliser des lambdas qui n'ont pas de comportement différent des fonctions qu'ils encapsulent.

Jack
la source
1
Je pense que ce que mon collègue et moi discutons, c'est davantage ces deux affectations: Predicate<Result> pred = this::isResultInFuture;et Predicate<Result> pred = resultInFuture();où resultInFuture () renvoie a Predicate<Result>. Donc, bien que ce soit une excellente explication du moment où utiliser la référence de méthode par rapport à une expression lambda, cela ne couvre pas tout à fait ce troisième cas.
M. Justin
4
@ M.Justin: Le resultInFuture();devoir est identique au devoir lambda que j'ai présenté, sauf que dans mon cas votre resultInFuture()méthode est en ligne. Cette approche a donc deux couches d'indirection (dont aucune ne fait rien) - la méthode de construction lambda et la lambda elle-même. Encore pire!
Jack
5

Aucune des réponses actuelles ne répond réellement au cœur de la question, à savoir si la classe doit avoir une private boolean isResultInFuture(Result result)méthode ou une private Predicate<Result> resultInFuture()méthode. Bien qu'ils soient largement équivalents, chacun présente des avantages et des inconvénients.

La première approche est plus naturelle si vous appelez directement la méthode en plus de créer une référence de méthode. Par exemple, si vous testez à l'unité cette méthode, il pourrait être plus facile d'utiliser la première approche. Un autre avantage de la première approche est que la méthode n'a pas à se soucier de son utilisation, la découplant de son site d'appel. Si vous modifiez plus tard la classe pour appeler directement la méthode, ce découplage sera très rentable.

La première approche est également supérieure si vous souhaitez adapter cette méthode à différentes interfaces fonctionnelles ou à différentes unions d'interfaces. Par exemple, vous pouvez souhaiter que la référence de méthode soit de type Predicate<Result> & Serializable, ce que la deuxième approche ne vous donne pas la flexibilité nécessaire.

En revanche, la seconde approche est plus naturelle si vous avez l'intention d'effectuer des opérations d'ordre supérieur sur le prédicat. Le prédicat contient plusieurs méthodes qui ne peuvent pas être appelées directement sur une référence de méthode. Si vous avez l'intention d'utiliser ces méthodes, la deuxième approche est supérieure.

En fin de compte, la décision est subjective. Mais si vous n'avez pas l'intention d'appeler des méthodes sur Predicate, je préférerais préférer la première approche.

Réintégrer Monica
la source
Des opérations d'ordre supérieur sur le prédicat sont toujours disponibles en lançant la référence de méthode:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack
4

Je n'écris plus de code Java, mais j'écris dans un langage fonctionnel pour vivre, et je soutiens également d'autres équipes qui apprennent la programmation fonctionnelle. Les lambdas ont une délicate douceur de lisibilité. Le style généralement accepté pour eux est que vous les utilisez lorsque vous pouvez les aligner, mais si vous ressentez le besoin de l'attribuer à une variable pour une utilisation ultérieure, vous devez probablement simplement définir une méthode.

Oui, les gens assignent des lambdas aux variables dans les tutoriels tout le temps. Ce ne sont que des didacticiels pour vous donner une compréhension approfondie de leur fonctionnement. Ils ne sont pas réellement utilisés de cette façon dans la pratique, sauf dans des circonstances relativement rares (comme choisir entre deux lambdas en utilisant une expression if).

Cela signifie que si votre lambda est assez court pour être facilement lisible après l'inlining, comme l'exemple du commentaire de @JimmyJames, alors allez-y:

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

Comparez cela avec la lisibilité lorsque vous essayez d'inclure votre exemple lambda:

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

Pour votre exemple particulier, je le signalerais personnellement sur une demande d'extraction et vous demanderais de le retirer vers une méthode nommée en raison de sa longueur. Java est un langage très verbeux, donc peut-être qu'avec le temps ses propres pratiques spécifiques au langage différeront des autres langages, mais jusque-là, gardez vos lambdas courts et alignés.

Karl Bielefeldt
la source
2
Re: verbosité - Bien que les nouveaux ajouts fonctionnels ne réduisent pas en eux-mêmes beaucoup de verbosité, ils permettent des façons de le faire. Par exemple, vous pouvez définir: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)avec l'implémentation évidente, puis vous pouvez écrire du code comme celui-ci: ce people = sort(people, (a,b) -> b.age() - a.age());qui est une grande amélioration OMI.
JimmyJames
C'est un excellent exemple de ce dont je parlais, @JimmyJames. Lorsque les lambdas sont courts et peuvent être alignés, ils constituent une grande amélioration. Quand ils deviennent si longs que vous voulez les séparer et leur donner un nom, il vaut mieux définir une méthode. Merci de m'avoir aidé à comprendre comment ma réponse a été mal interprétée.
Karl Bielefeldt
Je pense que votre réponse était très bien. Je ne sais pas pourquoi il serait rejeté.
JimmyJames