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>
à T
alors 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é I
est "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é I
est de type U.)
La version séquentielle de foldLeft
est exactement comme la version séquentielle de reduce
sauf que les valeurs intermédiaires sont de type U au lieu de type T. Mais c'est par ailleurs la même chose. (Une foldRight
opé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 foldLeft
et foldRight
opé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.