Pourquoi un combineur est-il nécessaire pour la méthode de réduction qui convertit le type en Java 8


142

J'ai du mal à comprendre pleinement le rôle que combinerremplit la reduceméthode Streams .

Par exemple, le code suivant ne compile pas:

int length = asList("str1", "str2").stream()
            .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());

Erreur de compilation dit: (discordance d'argument; int ne peut pas être converti en java.lang.String)

mais ce code compile:

int length = asList("str1", "str2").stream()  
    .reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(), 
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);

Je comprends que la méthode de combinaison est utilisée dans des flux parallèles - donc dans mon exemple, elle additionne deux entiers accumulés intermédiaires.

Mais je ne comprends pas pourquoi le premier exemple ne se compile pas sans le combineur ou comment le combineur résout la conversion de chaîne en int puisqu'il ne fait qu'ajouter deux entiers.

Quelqu'un peut-il faire la lumière sur cette question?



2
aha, c'est pour les flux parallèles ... J'appelle l'abstraction qui fuit!
Andy

Réponses:


77

Les versions à deux et trois arguments reduceque vous avez essayé d'utiliser n'acceptent pas le même type pour le accumulator.

Les deux arguments reducesont définis comme :

T reduce(T identity,
         BinaryOperator<T> accumulator)

Dans votre cas, T est String, donc BinaryOperator<T>devrait accepter deux arguments String et renvoyer une String. Mais vous lui passez un int et une chaîne, ce qui entraîne l'erreur de compilation que vous avez - argument mismatch; int cannot be converted to java.lang.String. En fait, je pense que passer 0 comme valeur d'identité est également incorrect ici, car une chaîne est attendue (T).

Notez également que cette version de reduction traite un flux de Ts et renvoie un T, vous ne pouvez donc pas l'utiliser pour réduire un flux de String en un int.

Les trois arguments reducesont définis comme :

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

Dans votre cas, U est Integer et T est String, donc cette méthode réduira un flux de String à un Integer.

Pour l' BiFunction<U,? super T,U>accumulateur, vous pouvez passer des paramètres de deux types différents (U et? Super T), qui dans votre cas sont Integer et String. De plus, la valeur d'identité U accepte un Integer dans votre cas, donc lui passer 0 est très bien.

Une autre façon de réaliser ce que vous voulez:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .reduce(0, (accumulatedInt, len) -> accumulatedInt + len);

Ici, le type du flux correspond au type de retour de reduce, vous pouvez donc utiliser la version à deux paramètres de reduce.

Bien sûr, vous n'avez pas du tout à utiliser reduce:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();

8
Comme deuxième option dans votre dernier code, vous pouvez également utiliser mapToInt(String::length)over mapToInt(s -> s.length()), je ne sais pas si l'un serait meilleur que l'autre, mais je préfère le premier pour la lisibilité.
skiwi

20
Beaucoup trouveront cette réponse car ils ne comprennent pas pourquoi le combinerest nécessaire, pourquoi ne pas avoir le accumulatorsuffit. Dans ce cas: Le combineur n'est nécessaire que pour les flux parallèles, pour combiner les résultats "accumulés" des threads.
ddekany

1
Je ne trouve pas votre réponse particulièrement utile - parce que vous n'expliquez pas du tout ce que le combineur doit faire et comment je peux travailler sans lui! Dans mon cas, je veux réduire un type T en U mais il n'y a aucun moyen que cela puisse être fait en parallèle du tout. Ce n'est tout simplement pas possible. Comment dire au système que je n'ai pas envie / besoin de parallélisme et ainsi laisser de côté le combineur?
Zordid

@Zordid l'API Streams n'inclut pas d'option pour réduire le type T en U sans passer un combineur.
Eran

216

La réponse d'Eran a décrit les différences entre les versions reduceà deux et trois arguments de en ce que la première se réduit Stream<T>à Talors que la seconde se réduit Stream<T>à U. Cependant, cela n'expliquait pas réellement la nécessité de la fonction de combinateur supplémentaire lors de la réduction Stream<T>à U.

L'un des principes de conception de l'API Streams est que l'API ne doit pas différer entre les flux séquentiels et parallèles, ou en d'autres termes, une API particulière ne doit pas empêcher un flux de s'exécuter correctement de manière séquentielle ou parallèle. Si vos lambdas ont les bonnes propriétés (associatives, non interférentes, etc.), un flux exécuté séquentiellement ou en parallèle devrait donner les mêmes résultats.

Considérons d'abord la version à deux arguments de la réduction:

T reduce(I, (T, T) -> T)

La mise en œuvre séquentielle est simple. La valeur d'identité Iest "accumulée" avec l'élément de flux zéro pour donner un résultat. Ce résultat est accumulé avec le premier élément de flux pour donner un autre résultat, qui à son tour est accumulé avec le deuxième élément de flux, et ainsi de suite. Une fois le dernier élément accumulé, le résultat final est renvoyé.

La mise en œuvre parallèle commence par diviser le flux en segments. Chaque segment est traité par son propre thread de la manière séquentielle que j'ai décrite ci-dessus. Maintenant, si nous avons N threads, nous avons N résultats intermédiaires. Celles-ci doivent être réduites à un seul résultat. Puisque chaque résultat intermédiaire est de type T, et que nous en avons plusieurs, nous pouvons utiliser la même fonction d'accumulateur pour réduire ces N résultats intermédiaires à un seul résultat.

Considérons maintenant une opération de réduction hypothétique à deux arguments qui se réduit Stream<T>à U. Dans d'autres langues, cela s'appelle une opération «plier» ou «plier à gauche», c'est ainsi que je l'appellerai ici. Notez que cela n'existe pas en Java.

U foldLeft(I, (U, T) -> U)

(Notez que la valeur d'identité Iest de type U.)

La version séquentielle de foldLeftest exactement comme la version séquentielle de reducesauf que les valeurs intermédiaires sont de type U au lieu de type T. Mais c'est par ailleurs la même chose. (Une foldRightopération hypothétique serait similaire, sauf que les opérations seraient effectuées de droite à gauche au lieu de gauche à droite.)

Considérons maintenant la version parallèle de foldLeft. Commençons par diviser le flux en segments. On peut alors demander à chacun des N threads de réduire les valeurs T de son segment en N valeurs intermédiaires de type U. Et maintenant? Comment passer de N valeurs de type U à un seul résultat de type U?

Ce qui manque, c'est une autre fonction qui combine les multiples résultats intermédiaires de type U en un seul résultat de type U.Si nous avons une fonction qui combine deux valeurs U en une seule, c'est suffisant pour réduire n'importe quel nombre de valeurs à un - tout comme la réduction originale ci-dessus. Ainsi, l'opération de réduction qui donne un résultat d'un type différent nécessite deux fonctions:

U reduce(I, (U, T) -> U, (U, U) -> U)

Ou, en utilisant la syntaxe Java:

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

En résumé, pour effectuer une réduction parallèle à un type de résultat différent, nous avons besoin de deux fonctions: une qui accumule les éléments T en valeurs U intermédiaires et une seconde qui combine les valeurs U intermédiaires en un seul résultat U. Si nous ne changeons pas de type, il s'avère que la fonction d'accumulateur est la même que la fonction de combineur. C'est pourquoi la réduction au même type n'a que la fonction d'accumulateur et la réduction à un type différent nécessite des fonctions d'accumulateur et de combinateur séparées.

Enfin, Java ne fournit pas foldLeftet foldRightopérations parce qu'elles impliquent un ordre particulier des opérations qui est par nature séquentielle. Cela est en contradiction avec le principe de conception énoncé ci-dessus de fournir des API qui prennent en charge le fonctionnement séquentiel et parallèle de la même manière.


7
Alors, que pouvez-vous faire si vous avez besoin d'un foldLeftcar le calcul dépend du résultat précédent et ne peut pas être parallélisé?
amoebe

5
@amoebe Vous pouvez implémenter votre propre foldLeft en utilisant forEachOrdered. L'état intermédiaire doit cependant être conservé dans une variable capturée.
Stuart marque

@StuartMarks merci, j'ai fini par utiliser jOOλ. Ils ont une mise en œuvrefoldLeft soignée de .
amoebe

1
J'adore cette réponse! Corrigez-moi si je me trompe: cela explique pourquoi l'exemple en cours d'exécution d'OP (le deuxième) n'invoquera jamais le combineur, lorsqu'il est exécuté, étant le flux séquentiel.
Luigi Cortese

2
Cela explique presque tout ... sauf: pourquoi cela devrait-il exclure la réduction séquentielle. Dans mon cas, il est IMPOSSIBLE de le faire en parallèle car ma réduction réduit une liste de fonctions en un U en appelant chaque fonction sur le résultat intermédiaire du résultat de ses prédécesseurs. Cela ne peut pas du tout être fait en parallèle et il n'y a aucun moyen de décrire un combineur. Quelle méthode puis-je utiliser pour y parvenir?
Zordid

116

Puisque j'aime les griffonnages et les flèches pour clarifier les concepts ... commençons!

From String to String (flux séquentiel)

Supposons que vous ayez 4 chaînes: votre objectif est de concaténer ces chaînes en une seule. En gros, vous commencez par un type et terminez par le même type.

Vous pouvez y parvenir avec

String res = Arrays.asList("one", "two","three","four")
        .stream()
        .reduce("",
                (accumulatedStr, str) -> accumulatedStr + str);  //accumulator

et cela vous aide à visualiser ce qui se passe:

entrez la description de l'image ici

La fonction d'accumulateur convertit, étape par étape, les éléments de votre flux (rouge) en valeur finale réduite (verte). La fonction accumulateur transforme simplement un Stringobjet en un autre String.

From String to int (flux parallèle)

Supposons que vous ayez les mêmes 4 chaînes: votre nouvel objectif est de faire la somme de leurs longueurs et vous souhaitez paralléliser votre flux.

Ce dont vous avez besoin est quelque chose comme ceci:

int length = Arrays.asList("one", "two","three","four")
        .parallelStream()
        .reduce(0,
                (accumulatedInt, str) -> accumulatedInt + str.length(),                 //accumulator
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner

et c'est un schéma de ce qui se passe

entrez la description de l'image ici

Ici, la fonction accumulateur (a BiFunction) vous permet de transformer vos Stringdonnées en intdonnées. Étant le flux parallèle, il est divisé en deux parties (rouges), dont chacune est élaborée indépendamment l'une de l'autre et produit autant de résultats partiels (orange). La définition d'un combineur est nécessaire pour fournir une règle de fusion des intrésultats partiels dans le résultat final (vert) int.

From String to int (flux séquentiel)

Et si vous ne souhaitez pas paralléliser votre flux? Eh bien, un combineur doit quand même être fourni, mais il ne sera jamais invoqué, étant donné qu'aucun résultat partiel ne sera produit.


7
Merci pour cela. Je n'avais même pas besoin de lire. J'aurais aimé qu'ils viennent d'ajouter une fonction de pli effrayante.
Lodewijk Bogaards

1
@LodewijkBogaards heureux que cela ait aidé! JavaDoc est en effet assez cryptique
Luigi Cortese

@LuigiCortese Dans le flux parallèle, divise-t-il toujours les éléments en paires?
TheLogicGuy

1
J'apprécie votre réponse claire et utile. Je veux répéter un peu ce que vous avez dit: "Eh bien, un combineur doit être fourni de toute façon, mais il ne sera jamais invoqué." Cela fait partie de la programmation fonctionnelle de Brave New World of Java qui, m'a-t-on assuré d'innombrables fois, «rend votre code plus concis et plus facile à lire». Espérons que les exemples de clarté concise (citations au doigt) comme celle-ci restent rares.
navette

Il sera BEAUCOUP mieux d'illustrer réduire à huit cordes ...
Ekaterina Ivanova iceja.net

0

Il n'y a pas de version réduite qui prend deux types différents sans combineur car elle ne peut pas être exécutée en parallèle (je ne sais pas pourquoi c'est une exigence). Le fait que l' accumulateur doive être associatif rend cette interface quasiment inutile puisque:

list.stream().reduce(identity,
                     accumulator,
                     combiner);

Produit les mêmes résultats que:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);

Une telle mapastuce dépend du particulier accumulatoret combinerpeut ralentir sensiblement les choses.
Tagir Valeev

Ou, accélérez-le considérablement puisque vous pouvez maintenant simplifier accumulatoren supprimant le premier paramètre.
quiz123

Une réduction parallèle est possible, cela dépend de votre calcul. Dans votre cas, vous devez être conscient de la complexité du combineur mais aussi de l'accumulateur sur l'identité par rapport aux autres instances.
LoganMzz
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.