Чому потрібен комбайнер для зменшення методу, який перетворює тип у java 8


142

У мене виникають труднощі в повному розумінні ролі, яку combinerвиконує reduceметод Streams .

Наприклад, не компілюється такий код:

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

Помилка компіляції говорить: (невідповідність аргументу; int не може бути перетворений у java.lang.String)

але цей код справді компілюється:

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

Я розумію, що метод комбінатора використовується в паралельних потоках - тому в моєму прикладі це поєднання двох проміжних накопичених ints.

Але я не розумію, чому перший приклад не компілюється без комбінатора чи як комбінатор вирішує перетворення рядка в int, оскільки він просто додає два ints.

Хтось може пролити світло на це?


Пов'язаний питання: stackoverflow.com/questions/24202473 / ...
nosid

2
ага, це для паралельних потоків ... Я називаю нещільну абстракцію!
Енді

Відповіді:


77

Дві та три версії аргументів, reduceякі ви намагалися використати, не приймають однаковий тип для accumulator.

Два аргументи reduceбуде визначено як :

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

У вашому випадку T є String, тому BinaryOperator<T>слід прийняти два аргументи String і повернути String. Але ви передаєте йому int та String, що призводить до помилки компіляції, яку ви отримали - argument mismatch; int cannot be converted to java.lang.String. Насправді, я думаю, що передача 0 як значення ідентичності тут також неправильна, оскільки очікується рядок (T).

Також зауважте, що ця версія скорочення обробляє потік Ts і повертає T, тому ви не можете використовувати його для зменшення потоку String до int.

Три аргументи reduceбуде визначено як :

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

У вашому випадку U є цілим, а T - рядком, тому цей метод зменшить потік String до цілого числа.

Для BiFunction<U,? super T,U>акумулятора ви можете передавати параметри двох різних типів (U і? Super T), які у вашому випадку є Integer і String. Крім того, значення ідентичності U приймає цілий число у вашому випадку, тому передача його 0 - це добре.

Ще один спосіб досягти бажаного:

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

Тут тип потоку відповідає типу повернення reduce, тому ви можете використовувати два параметри версії reduce.

Звичайно, вам взагалі не потрібно користуватися reduce:

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

8
Як другий варіант у вашому останньому коді, ви також можете використовувати mapToInt(String::length)over mapToInt(s -> s.length()), не впевнений, чи буде один кращий за інший, але я віддаю перевагу першому для читабельності.
skiwi

20
Багато хто знайде цю відповідь, оскільки не отримує, чому combinerпотрібне, чому не accumulatorвистачає. У такому випадку: комбайнер потрібен лише для паралельних потоків, щоб поєднати "накопичені" результати ниток.
декани

1
Я не вважаю вашу відповідь особливо корисною - адже ви зовсім не пояснюєте, що повинен робити комбайнер і як я можу працювати без цього! У моєму випадку я хочу зменшити тип T до U, але немає жодного способу зробити це паралельно. Це просто неможливо. Як ви скажете системі, що я не хочу / не потребую паралелізму, і, таким чином, залишати комбайнер?
Зордід

@ Zordid Streams API не включає опцію зменшення типу T до U без проходження комбінатора.
Еран

216

У відповіді Ерана описані відмінності між двома аргументами та трьома аргументами версій 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, які однаково підтримують послідовну та паралельну роботу.


7
То що ви можете зробити, якщо вам потрібно, foldLeftтому що обчислення залежить від попереднього результату і не може бути паралельне?
амебе

5
@amoebe Ви можете реалізувати власну foldLeft за допомогою forEachOrdered. Однак проміжний стан має зберігатися у захопленій змінній.
Стюарт Маркс

@StuartMarks дякую, я закінчив використовувати jOOλ. Вони акуратно реалізуютьfoldLeft .
амебе

1
Любіть цю відповідь! Виправте мене, якщо я помиляюся: це пояснює, чому приклад запуску OP (другий) ніколи не посилатиметься на комбайнер, коли він працює, будучи потоком послідовним.
Луїджі Кортез

2
Це пояснює майже все ... крім: чому це повинно виключати послідовно скорочене скорочення. У моєму випадку неможливо робити це паралельно, оскільки моє скорочення скорочує список функцій в U, викликаючи кожну функцію на проміжний результат попередніх результатів. Це взагалі неможливо зробити паралельно, і немає можливості описати комбайнер. Який метод я можу використовувати для цього?
Зордід

116

Оскільки я люблю каракулі та стрілки для уточнення понять ... почнемо!

Від рядка до рядка (послідовний потік)

Припустимо, має 4 рядки: ваша мета - об'єднати такі рядки в одну. Ви в основному починаєте з типу і закінчуєте тим же типом.

Ви можете досягти цього за допомогою

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

і це допоможе вам уявити, що відбувається:

введіть тут опис зображення

Функція акумулятора крок за кроком перетворює елементи вашого (червоного) потоку в остаточне зменшене (зелене) значення. Акумуляторна функція просто перетворює Stringоб’єкт в інший String.

Від String до int (паралельний потік)

Припустимо, у вас однакові 4 рядки: ваша нова мета - підсумовувати їх довжини, і ви хочете паралелізувати свій потік.

Вам потрібно щось подібне:

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

і це схема того, що відбувається

введіть тут опис зображення

Тут функція акумулятора (a BiFunction) дозволяє перетворити ваші Stringдані в intдані. Будучи потоком паралельним, він розбивається на дві (червоні) частини, кожна з яких розробляється незалежно від інших і дає стільки ж часткових (помаранчевих) результатів. Визначення комбінатора необхідно, щоб забезпечити правило для об'єднання часткових intрезультатів у остаточний (зелений) int.

Від String до int (послідовний потік)

Що робити, якщо ви не хочете паралелізувати свій потік? Ну, комбайнер у будь-якому випадку потрібно надати, але його не буде використано, якщо часткових результатів не буде.


7
Дякую за це Мені навіть не потрібно було читати. Я б хотів, щоб вони просто додали функцію вигадливих складок.
Lodewijk Bogaards

1
@LodewijkBogaards радий, що це допомогло! JavaDoc тут справді досить виразний
Луїджі Кортез

@LuigiCortese У паралельному потоці він завжди розділяє елементи на пари?
TheLogicGuy

1
Я ціную вашу чітку і корисну відповідь. Я хочу ще раз повторити те, що ви сказали: "Ну, комбайнер у будь-якому випадку потрібно забезпечити, але він ніколи не посилатиметься". Це частина функціонального програмування Brave New World of Java, який, як я вже неодноразово запевняв, "робить ваш код більш стислим і легшим для читання". Будемо сподіватися, що таких прикладів (цитат пальців) лаконічної чіткості, як це, залишається мало і далеко між ними.
Ганчірка

Набагато краще буде проілюструвати зменшення за допомогою восьми рядків ...
Катерина Іванова iceja.net

0

Не існує версії скорочення, яка б використовувала два різні типи без комбайнера, оскільки вона не може виконуватися паралельно (не впевнений, чому це вимога). Той факт, що акумулятор повинен бути асоціативним, робить цей інтерфейс набагато марним, оскільки:

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

Дає ті самі результати, що і:

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

Такий mapтрюк залежно від конкретного accumulatorта combinerможе значно уповільнити речі.
Тагір Валєєв

Або прискорити його значно, оскільки тепер ви можете спростити accumulator, скинувши перший параметр.
вікторина123

Можливе паралельне зменшення, це залежить від вашого обчислення. У вашому випадку ви повинні знати про складність комбінатора, але також і накопичувача в ідентичності проти інших випадків.
LoganMzz
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.