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

113

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

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

Et si cela ne suffit pas et que j'ai besoin de TriFunction? Ça 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 juste de comprendre la raison de ne pas l'inclure dans la bibliothèque standard.

Richard Finegan
la source
1
avec l'interface bifonction, vous pouvez facilement définir la classe N-fonction, si vous définissez la trifonction comme interface séparée, le premier sb vous demandera pourquoi pas le quadofonction, et deuxièmement, vous devez dupliquer toutes les méthodes qui prennent Bifunction comme paramètre
user902383
6
Il y a un point de rendement décroissant pour des API comme celle-ci. (Personnellement, je pense que JDK8 l'a passé il y a quelque temps, mais c'est même au-delà de cela.)
Louis Wasserman
Je pense que la logique était de dire que Function et BiFunction étaient entièrement implémentées avec des objets et des types natifs. Inclure des TriFunctions avec toutes les différentes variantes ferait exploser le JRE avec des classes et des méthodes.
Thorbjørn Ravn Andersen
1
Réponse courte. En Java, si vous ne le voyez pas, vous créez le vôtre (voir les réponses d'Alex P bien sûr). Sidenote, en C #, les implémenteurs de dotnet vous en ont donné des pré-cannés (jusqu'à 16 arguments), mais sans les noms de préfixes ("Bi" ici): voir docs.microsoft.com/en-us/dotnet/api/... Juste un simple "Func". C'est donc l'un des endroits où je préfère dotnet à java. Veuillez ne pas transformer cette section de commentaires en une guerre h0ly. et limiter les commentaires à BiFunction uniquement.
granadaCoder

Réponses:

81

Autant que je sache, il n'y a que deux types de fonctions, destructives et constructives.

Alors que la fonction constructive, comme son nom l'indique, construit quelque chose, une fonction destructrice détruit quelque chose, mais pas de la manière que vous pensez actuellement.

Par exemple, la fonction

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

est une question constructive . 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 une dispute ouverte. Vous ne pouvez pas simplement dire "eh bien, laissez x: = 1" et essayer chaque y que vous voudrez peut-être essayer. Vous devez construire à chaque fois le tuple entier avec x := 1. Donc, si vous aimez voir ce que les fonctions renvoient, y := 1, y := 2, y := 3vous devez écrire f(1,1) , f(1,2) , f(1,3).

Dans 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. Ce sont un peu comme des méthodes statiques. Vous pouvez les utiliser, mais ils n'ont aucun état réel.

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

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

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

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

Les modèles comme Factory sont beaucoup plus faciles si vous pouvez simplement ajouter une fonction qui fait le travail pour vous.

Les destructeurs se combinent facilement les uns avec les autres. Si le type est correct, vous pouvez simplement les composer comme vous le souhaitez. En utilisant cela, vous pouvez facilement définir des morphismes qui facilitent (avec des valeurs immuables) les tests!

Vous pouvez le faire aussi avec une composition constructive, mais la composition destructive ressemble plus à une liste ou à un décorateur, et 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 en "retour arrière", utilisez simplement l'ancienne fonction destructive. Cela rend le code beaucoup plus petit et plus lisible. Avec les fonctions constructives, vous avez plus ou moins à vous souvenir de tous les arguments, ce qui peut être beaucoup.

Alors, pourquoi y a-t-il un besoin de BiFunctiondevrait être plus question que pourquoi il n'y en a pas TriFunction?

Tout d'abord, la plupart du temps, vous n'avez que quelques valeurs (moins de 3) et n'avez besoin que d'un résultat, donc une fonction destructive normale ne serait pas du tout nécessaire, une fonction constructive ferait bien l'affaire. Et il y a des choses comme les monades qui ont vraiment besoin d'une fonction constructive. Mais à part ça, il n'y a pas vraiment beaucoup de bonnes raisons pour lesquelles il y en a BiFunctiondu tout. Ce qui ne veut pas dire qu'il devrait être supprimé! Je me bats pour mes Monades jusqu'à ma mort!

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

user3003859
la source
2
Vous avez répondu à ma question ... Je pense ... Je ne sais pas si les concepteurs du langage Java viennent de cette ligne de pensée, mais je ne suis pas très familiarisé avec la programmation fonctionnelle. Merci pour l'explication.
Richard Finegan
79
Je n'ai jamais vu les termes constructif et destructeur être utilisés pour désigner les concepts que vous décrivez. Je pense que curry et non curry sont des termes plus courants.
Feuermurmel
17
Le premier exemple de fonction n'est pas syntaxiquement correct. Cela devrait être BiFunction et non Function, car il prend deux arguments d'entrée.
annouk le
3
IMO a BiFunctionété créé pour permettre une réduction aisée des données, et la plupart Streamdes opérations de terminal ne sont que des réductions de données. Un bon exemple est BinaryOperator<T>, utilisé dans beaucoup Collectors. Un premier élément est réduit avec le second, puis peut être réduit avec le suivant, et ainsi de suite. Bien sûr, vous pouvez créer un Function<T, Function<T, T>func = x -> (y -> / * code de réduction ici * /). Mais sérieusement? Tout cela quand vous pouvez le faire simplement BinaryOperator<T> func = (x, y) -> /*reduction code here*/. De plus, cette approche de réduction des données ressemble beaucoup à votre approche «destructive» pour moi.
FBB
32
Comment cela a-t-il obtenu autant de votes positifs? C'est une réponse terrible et déroutante, car elle est basée sur le principe que Function<Integer,Integer> f = (x,y) -> x + yJava est valide, ce qui n'est pas le cas. Cela devrait être une BiFonction pour commencer!
wvdz
162

Si vous avez besoin de TriFunction, procédez comme suit:

@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é. N'oubliez pas que le type de résultat est spécifié comme 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.*ou java.lang.*il aurait été défini. Je n'irais jamais au-delà de 22 arguments, cependant ;-) Ce que je veux dire par là, tout nouveau code qui permet de diffuser des collections n'a jamais eu besoin de TriFunction comme l'un des paramètres de la méthode. Cela n'a donc pas été inclus.

METTRE À JOUR

Pour être complet et en suivant l'explication des fonctions destructives dans une autre réponse (liée au currying), voici comment 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 des fonctions d'autres manières, 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 curry serait naturel pour tout langage qui prend en charge 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 lu. Cependant, il est très utile comme exercice, et parfois des fonctions partielles ont une place légitime dans votre code.

Alex Pakka
la source
6
Merci pour la solution. Et oui, il y a certainement une utilisation pour BiFunction, TriFunction, ... Sinon, les gens ne le chercheraient pas. Il suppose que tout le problème lambda est tout simplement trop nouveau pour Oracle pour le moment et sera étendu dans les versions Java ultérieures. Pour le moment, c'est plus une preuve de concept.
Stefan Endrullis
Hy @Alex pouvez-vous s'il vous plaît définir la ligne suivante. ce qui se passe ici par défaut <V> TriFunction <A, B, C, V> andThen (Function <? super R,? extend V> after) {Objects.requireNonNull (after); return (A a, B b, C c) -> after.apply (apply (a, b, c)); }
Muneeb Nasir
@MuneebNasir - il vous permet de faire la composition des fonctions: TriFunction<Integer,Integer,Integer,Integer> comp = (x,y,z) -> x + y + z; comp = comp.andThen(s -> s * 2); int result = comp.apply(1, 2, 3); //12Voir stackoverflow.com/questions/19834611/…
Alex Pakka
andThen()Exemple d'utilisation ajouté à la réponse.
Alex Pakka du
Non seulement le curry n'est pas bien adapté au langage Java, mais aussi, corrigez-moi si je me trompe, mais il BiFunctionest utilisé dans l' StreamAPI pour effectuer une réduction des données, ce qui ressemble beaucoup à l'approche du curry pour moi: vous n'en prenez jamais plus de deux arguments, et vous pouvez traiter n'importe quel nombre d'éléments, une réduction à la fois (voir mon commentaire sur la réponse acceptée, je serais heureux de savoir si je me trompe de voir les choses de cette façon).
FBB
13

L'alternative est, ajoutez 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;
Amol Damodar
la source
2
J'étais sur le point de mettre à jour ma réponse pour mentionner vavr, mais vous étiez le premier, alors j'ai voté pour. Si vous arrivez au point où vous avez besoin d'une TriFunction, il y a de grandes chances que vous soyez mieux en utilisant la vavrbibliothèque - cela rend la programmation de style fonctionnel aussi supportable que possible en Java.
Alex Pakka
7

J'ai presque la même question et une réponse partielle. Je ne sais pas si la réponse constructive / déconstructive est ce que les concepteurs de langage avaient en tête. Je pense qu'avoir 3 et plus jusqu'à N a des cas d'utilisation valides.

Je viens de .NET. et dans .NET vous avez Func et Action pour les fonctions void. Prédicat 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 de langage ont opté pour Function, Bifunction et n'ont pas continué jusqu'à DecaExiFunction?

La réponse à la deuxième partie est l'effacement de type. Après compilation, il n'y a aucune 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);
    }
}

Des fonctions internes ont été utilisées pour contourner un autre problème mineur. Eclipse a insisté pour avoir les deux classes dans des fichiers nommés Function dans le même répertoire ... Je ne sais pas si c'est un problème de compilateur de nos jours. Mais je ne peux pas transformer l'erreur de dans Eclipse.

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

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

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

Exemple pour la deuxième manière:

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

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

et

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 d'implémentations pour les méthodes andThen () et compose (). Si vous ajoutez ces derniers, vous devez ajouter 16 surcharges chacun: le TriFunc doit avoir un andthen () avec 16 arguments. Cela vous donnerait une erreur de compilation en raison des dépendances circulaires. De plus, vous n'auriez pas ces surcharges pour Function et BiFunction. Par conséquent, vous devez également définir Func avec un argument et Func avec deux arguments. Dans .NET, les dépendances circulaires seraient contournées en utilisant des méthodes d'extension qui ne sont pas présentes en Java.

Hans
la source
2
Pourquoi auriez-vous besoin andThende 16 arguments? Le résultat d'une fonction en Java est une valeur unique. andThenprend cette valeur et en fait quelque chose. De plus, il n'y a pas de problème de dénomination. Les noms de classe doivent être différents et se trouver dans des fichiers différents nommés de la même manière - en suivant la logique définie par les développeurs de langage Java avec Function et BiFunction. De plus, tous ces noms différents sont nécessaires si les types d'arguments sont différents. On peut créer un VargFunction(T, R) { R apply(T.. t) ... }seul type.
Alex Pakka
2

J'ai trouvé le code source de BiFunction ici:

https://github.com/JetBrains/jdk8u_jdk/blob/master/src/share/classes/java/util/function/BiFunction.java

Je l'ai modifié pour créer TriFunction. Comme BiFunction, il utilise andThen () et non compose (), donc pour certaines applications qui nécessitent compose (), cela peut ne pas être approprié. Cela devrait convenir aux types d'objets normaux. Un bon article sur andThen () et compose () peut être trouvé ici:

http://www.deadcoderising.com/2015-09-07-java-8-functional-composition-using-compose-and-andthen/

import java.util.Objects;
import java.util.function.Function;

/**
 * Represents a function that accepts two arguments and produces a result.
 * This is the three-arity specialization of {@link Function}.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object, Object)}.
 *
 * @param <S> the type of the first argument to the function
 * @param <T> the type of the second argument to the function
 * @param <U> the type of the third argument to the function
 * @param <R> the type of the result of the function
 *
 * @see Function
 * @since 1.8
 */
@FunctionalInterface
public interface TriFunction<S, T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param s the first function argument
     * @param t the second function argument
     * @param u the third function argument
     * @return the function result
     */
    R apply(S s, T t, U u);

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     */
    default <V> TriFunction<S, T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (S s, T t, U u) -> after.apply(apply(s, t, u));
    }
}
Vance
la source
2

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

@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
Leandro Maro
la source
0

Vous ne pouvez pas toujours vous arrêter à TriFunction. Parfois, vous devrez peut-être passer n nombre de paramètres à vos fonctions. Ensuite, l'équipe de support devra créer une QuadFunction pour corriger votre code. La solution à long terme serait de créer un objet avec les paramètres supplémentaires, puis d'utiliser la fonction ou la biFonction prête à l'emploi.

Koushik Roy
la source