158 votes

Java 8 : Où se trouve TriFunction (et kin) dans java.util.function ? Ou quelle est l'alternative ?

Je vois java.util.function.BiFunction, donc je peux le faire :

BiFunction<Integer, Integer, Integer> f = (x, y) -> { return 0; };

Et si ce n'était pas suffisant et que j'avais besoin de TriFunction ? Elle n'existe pas !

TriFunction<Integer, Integer, Integer, Integer> f = (x, y, z) -> { return 0; };

Je suppose que je devrais ajouter que je sais que je peux définir ma propre TriFunction, j'essaie simplement de comprendre la raison pour laquelle elle n'est pas incluse dans la bibliothèque standard.

204voto

Alex Pakka Points 2682

Si vous avez besoin de TriFunction, faites ça :

@FunctionalInterface
interface TriFunction<A,B,C,R> {

    R apply(A a, B b, C c);

    default <V> TriFunction<A, B, C, V> andThen(
                                Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (A a, B b, C c) -> after.apply(apply(a, b, c));
    }
}

Le petit programme suivant montre comment il peut être utilisé. Rappelez-vous que le type de résultat est spécifié comme un dernier paramètre de type générique.

  public class Main {

    public static void main(String[] args) {
        BiFunction<Integer, Long, String> bi = (x,y) -> ""+x+","+y;
        TriFunction<Boolean, Integer, Long, String> tri = (x,y,z) -> ""+x+","+y+","+z;

        System.out.println(bi.apply(1, 2L)); //1,2
        System.out.println(tri.apply(false, 1, 2L)); //false,1,2

        tri = tri.andThen(s -> "["+s+"]");
        System.out.println(tri.apply(true,2,3L)); //[true,2,3]
    }
  }

Je suppose que s'il y avait une utilisation pratique de TriFunction dans java.util.* o java.lang.* elle aurait été définie. Je n'irais jamais au-delà de 22 arguments, cependant ;-) Ce que je veux dire par là, c'est que tout le nouveau code qui permet de streamer des collections n'a jamais requis la TriFunction comme paramètre de méthode. Elle n'a donc pas été incluse.

UPDATE

Pour être complet et suite à l'explication des fonctions destructives dans une autre réponse (liée au currying), voici comment la TriFunction peut être émulée sans interface supplémentaire :

Function<Integer, Function<Integer, UnaryOperator<Integer>>> tri1 = a -> b -> c -> a + b + c;
System.out.println(tri1.apply(1).apply(2).apply(3)); //prints 6

Bien entendu, il est possible de combiner les fonctions d'une autre manière, par exemple :

BiFunction<Integer, Integer, UnaryOperator<Integer>> tri2 = (a, b) -> c -> a + b + c;
System.out.println(tri2.apply(1, 2).apply(3)); //prints 6
//partial function can be, of course, extracted this way
UnaryOperator partial = tri2.apply(1,2); //this is partial, eq to c -> 1 + 2 + c;
System.out.println(partial.apply(4)); //prints 7
System.out.println(partial.apply(5)); //prints 8

Alors que le currying serait naturel pour tout langage qui supporte la programmation fonctionnelle au-delà des lambdas, Java n'est pas construit de cette façon et, bien que réalisable, le code est difficile à maintenir, et parfois à lire. Cependant, il est très utile en tant qu'exercice, et parfois les fonctions partielles ont une place légitime dans votre code.

92voto

user3003859 Points 1114

Pour autant que je sache, il n'existe que deux types de fonctions, destructives et constructives.

Alors que la fonction constructive, comme son nom l'indique, construit quelque chose, la fonction destructive détruit quelque chose, mais pas de la manière dont vous pouvez le penser maintenant.

Par exemple, la fonction

Function<Integer,Integer> f = (x,y) -> x + y  

est un constructive un. Comme vous avez besoin de construire quelque chose. Dans l'exemple vous avez construit le tuple (x,y) . Les fonctions constructives ont le problème, de ne pas pouvoir gérer des arguments infinis. Mais le pire, c'est que vous ne pouvez pas simplement laisser un argument ouvert. On ne peut pas juste dire "bien, laissez x := 1" et essayer tous les y que vous pourriez vouloir essayer. Vous devez construire à chaque fois le tuple complet avec x := 1 . Donc si vous voulez voir ce que les fonctions retournent pour y := 1, y := 2, y := 3 vous devez écrire f(1,1) , f(1,2) , f(1,3) .

En Java 8, les fonctions constructives doivent être gérées (la plupart du temps) en utilisant des références de méthode, car il n'y a pas beaucoup d'avantages à utiliser une fonction lambda constructive. Elles sont un peu comme les méthodes statiques. Vous pouvez les utiliser, mais elles n'ont pas d'état réel.

L'autre type est le type destructeur, il prend quelque chose et le démonte autant que nécessaire. Par exemple, le destructeur fonction

Function<Integer, Function<Integer, Integer>> g = x -> (y -> x + y) 

fait la même chose que la fonction f ce qui était constructif. Les avantages d'une fonction destructive sont, vous pouvez gérer maintenant des arguments infinis, ce qui est particulièrement pratique pour les flux, et vous pouvez simplement laisser des arguments ouverts. Ainsi, si vous voulez à nouveau voir quel serait le résultat si x := 1 y y := 1 , y := 2 , y := 3 vous pouvez dire h = g(1) y h(1) est le résultat pour y := 1 , h(2) para y := 2 y h(3) para y := 3 .

Vous avez donc ici un état fixe ! C'est assez dynamique et c'est la plupart du temps ce que nous attendons d'un lambda.

Les modèles tels que Factory sont beaucoup plus simples si vous pouvez simplement intégrer une fonction qui fait le travail à votre place.

Les destructeurs se combinent facilement entre eux. Si le type est bon, vous pouvez les composer comme vous le souhaitez. En utilisant cela, vous pouvez facilement définir des morphismes qui rendent les tests (avec des valeurs immuables) beaucoup plus faciles !

Vous pouvez également le faire avec une composition constructive, mais la composition destructive ressemble davantage à une liste ou à un décorateur, tandis que la composition constructive ressemble beaucoup à un arbre. Et des choses comme le retour en arrière avec des fonctions constructives ne sont tout simplement pas agréables. Vous pouvez simplement sauvegarder les fonctions partielles d'une fonction destructive (programmation dynamique), et lors du "retour en arrière", utiliser l'ancienne fonction destructive. Cela rend le code beaucoup plus petit et plus lisible. Avec les fonctions constructives, vous devez plus ou moins vous souvenir de tous les arguments, ce qui peut être beaucoup.

Alors pourquoi faut-il BiFunction devrait être une question plus importante que la raison pour laquelle il n'y a pas de TriFunction ?

Tout d'abord, la plupart du temps, vous n'avez que quelques valeurs (moins de 3) et vous n'avez besoin que d'un résultat. Une fonction destructive normale ne serait donc pas nécessaire, une fonction constructive ferait l'affaire. Et il y a des choses comme les monades qui ont vraiment besoin d'une fonction constructive. Mais en dehors de cela, il n'y a pas vraiment beaucoup de bonnes raisons pour lesquelles il y a une fonction constructive. BiFunction du tout. Ce qui ne veut pas dire qu'il faut le supprimer ! Je me battrai pour mes Monads jusqu'à ma mort !

Donc, si vous avez beaucoup d'arguments, que vous ne pouvez pas combiner dans une classe conteneur logique, et si vous avez besoin de l'option fonction soit constructive, utilisez une référence de méthode. Sinon, essayez d'utiliser la nouvelle capacité acquise des fonctions destructives, vous pourriez vous retrouver à faire beaucoup de choses avec beaucoup moins de lignes de code.

21voto

Amol Damodar Points 1

L'alternative est d'ajouter la dépendance ci-dessous,

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

Maintenant, vous pouvez utiliser la fonction Vavr, comme ci-dessous jusqu'à 8 arguments,

3 arguments :

Function3<Integer, Integer, Integer, Integer> f = 
      (a, b, c) -> a + b + c;

5 arguments :

Function5<Integer, Integer, Integer, Integer, Integer, Integer> f = 
      (a, b, c, d, e) -> a + b + c + d + e;

8voto

Hans Points 457

J'ai presque la même question et une réponse partielle. Je ne suis pas sûr que la réponse constructive/déconstructive soit ce que les concepteurs du langage avaient à l'esprit. Je pense qu'avoir 3 et plus jusqu'à N a des cas d'utilisation valables.

Je viens de .NET. Et dans .NET vous avez Func et Action pour les fonctions vides. Predicate et quelques autres cas spéciaux existent également. Voir : https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Je me demande quelle est la raison pour laquelle les concepteurs du langage ont opté pour Function, Bifunction et n'ont pas continué jusqu'à DecaExiFunction ?

La réponse à la deuxième partie est l'effacement des caractères. Après la compilation, il n'y a pas de différence entre Func et Func. Ce qui suit ne compile donc pas :

package eu.hanskruse.trackhacks.joepie;

public class Functions{

    @FunctionalInterface
    public interface Func<T1,T2,T3,R>{
        public R apply(T1 t1,T2 t2,T3 t3);
    }

    @FunctionalInterface
    public interface Func<T1,T2,T3,T4,R>{
        public R apply(T1 t1,T2 t2,T3 t3, T4 t4);
    }
}

Les fonctions internes ont été utilisées pour contourner un autre problème mineur. Eclipse insistait pour que les deux classes soient placées dans des fichiers nommés Function dans le même répertoire... Je ne sais pas si c'est un problème de compilateur aujourd'hui. Mais je ne peux pas faire disparaître l'erreur dans Eclipse.

Func a été utilisé pour éviter les conflits de noms avec le type java Function.

Ainsi, si vous voulez ajouter un Func de 3 à 16 arguments, vous pouvez faire deux choses.

  • Fabrication de TriFunc, TesseraFunc, PendeFunc, ...DecaExiFunc, etc.
    • (Dois-je utiliser le grec ou le latin ?)
  • Utilisez des noms de paquets ou de classes pour que les noms soient différents.

Exemple pour la deuxième voie :

 package eu.hanskruse.trackhacks.joepie.functions.tri;

        @FunctionalInterface
        public interface Func<T1,T2,T3,R>{
            public R apply(T1 t1,T2 t2,T3 t3);
        }

y

package eu.trackhacks.joepie.functions.tessera;

    @FunctionalInterface
    public interface Func<T1,T2,T3,T4,R>{
        public R apply(T1 t1,T2 t2,T3 t3, T4 t4);
    }

Quelle serait la meilleure approche ?

Dans les exemples ci-dessus, je n'ai pas inclus les implémentations des méthodes andThen() et compose(). Si vous les ajoutez, vous devez ajouter 16 surcharges chacune : le TriFunc devrait avoir un andthen() avec 16 arguments. Cela vous donnerait une erreur de compilation à cause des dépendances circulaires. Vous n'auriez pas non plus ces surcharges pour Function et BiFunction. Par conséquent, vous devriez également définir Func avec un argument et Func avec deux arguments. Dans .NET, les dépendances circulaires peuvent être contournées en utilisant des méthodes d'extension qui n'existent pas en Java.

4voto

Leandro Maro Points 111

Vous pouvez également créer votre propre fonction en prenant les 3 paramètres suivants

@FunctionalInterface
public interface MiddleInterface<F,T,V>{
    boolean isBetween(F from, T to, V middleValue);
}

MiddleInterface<Integer, Integer, Integer> middleInterface = 
(x,y,z) -> x>=y && y<=z; // true

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X