У відповіді Ерана описані відмінності між двома аргументами та трьома аргументами версій reduce
у тому, що перший зводиться Stream<T>
до того, T
як другий зводиться Stream<T>
до U
. Однак насправді це не пояснило необхідність додаткової функції комбайнера при зменшенні Stream<T>
до U
.
Один із принципів дизайну API Streams полягає в тому, що API не повинен відрізнятися між послідовними та паралельними потоками, або, кажучи іншим способом, певний API не повинен перешкоджати правильному запуску потоку ні послідовно, ні паралельно. Якщо ваші лямбдахи мають правильні властивості (асоціативні, невтручаючі тощо), потік, який запускається послідовно або паралельно, повинен дати ті самі результати.
Спершу розглянемо дво аргументовану версію скорочення:
T reduce(I, (T, T) -> T)
Послідовна реалізація є простою. Значення ідентичності I
"накопичується" елементом нульового потоку, щоб дати результат. Цей результат накопичується разом з першим потоковим елементом, щоб отримати інший результат, який, у свою чергу, накопичується разом із другим елементом потоку тощо. Після накопичення останнього елемента кінцевий результат повертається.
Паралельна реалізація починається з розбиття потоку на сегменти. Кожен сегмент обробляється власною ниткою в послідовній моді, яку я описав вище. Тепер, якщо у нас є N потоків, ми маємо N проміжних результатів. Їх потрібно звести до одного результату. Оскільки кожен проміжний результат має тип T, а їх у нас декілька, ми можемо використовувати ту саму функцію акумулятора, щоб зменшити ці N проміжних результатів до одного результату.
Тепер розглянемо гіпотетичну операцію зменшення двох аргументів, яка зводиться Stream<T>
до U
. В інших мовах це називається операцією "складання" або "складання ліворуч", тому я тут це назву. Зауважте, цього в Java не існує.
U foldLeft(I, (U, T) -> U)
(Зверніть увагу, що значення ідентичності I
має тип U.)
Послідовна версія foldLeft
файлу подібна до послідовної версії, за reduce
винятком того, що проміжні значення мають тип U замість типу T. Але в іншому випадку це те саме. (Гіпотетична foldRight
операція була б аналогічною, за винятком того, що операції виконуватимуться справа наліво, а не зліва направо.)
Тепер розглянемо паралельну версію foldLeft
. Почнемо з розбиття потоку на сегменти. Тоді ми можемо з кожної з N потоків зменшити значення Т у своєму сегменті на N проміжних значень типу U. А тепер що? Як ми можемо отримати від N значень типу U до єдиного результату типу U?
Не вистачає ще однієї функції, яка поєднує кілька проміжних результатів типу U в єдиний результат типу U. Якщо у нас є функція, що поєднує два значення U в одне, цього достатньо, щоб зменшити будь-яке число значень до одного - точно так само початкове зменшення вище. Таким чином, операція скорочення, яка дає результат іншого типу, потребує двох функцій:
U reduce(I, (U, T) -> U, (U, U) -> U)
Або, використовуючи синтаксис Java:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Підсумовуючи це, для паралельного зведення до іншого типу результату нам потрібні дві функції: одна, що накопичує Т-елементи до проміжних значень U, і друга, що поєднує проміжні значення U в один U-результат. Якщо ми не перемикаємо типи, виявиться, що функція акумулятора така сама, як функція комбайнера. Ось чому зведення до одного типу має лише функцію акумулятора, а зведення до іншого типу вимагає окремих функцій акумулятора та комбайнера.
Нарешті, Java не забезпечує foldLeft
та виконує foldRight
операції, оскільки вони передбачають певне впорядкування операцій, яке за своєю суттю є послідовним. Це суперечить принципу проектування, зазначеному вище, щодо надання API, які однаково підтримують послідовну та паралельну роботу.