Отговорът на Еран описва разликите между версиите с два и три аргумента на reduce
в това, че първата редуцира Stream<T>
до T
, докато последният редуцира Stream<T>
до U
. Въпреки това, това всъщност не обяснява необходимостта от допълнителната функция за комбиниране при намаляване на Stream<T>
до U
.
Един от принципите на проектиране на API за потоци е, че 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 нишките да намали стойностите T в своя сегмент в 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)
В обобщение, за да направим паралелно редуциране до различен тип резултат, имаме нужда от две функции: една, която натрупва T елементи до междинни U стойности, и втора, която комбинира междинните U стойности в един U резултат. Ако не превключваме типовете, се оказва, че акумулаторната функция е същата като комбиниращата функция. Ето защо редуцирането до същия тип има само акумулираща функция, а редуцирането до различен тип изисква отделни акумулаторни и комбиниращи функции.
И накрая, Java не предоставя foldLeft
и foldRight
операции, защото те предполагат конкретно подреждане на операциите, което по своята същност е последователно. Това се сблъсква с посочения по-горе принцип на проектиране за предоставяне на API, които поддържат еднакво последователна и паралелна работа.
person
Stuart Marks
schedule
19.06.2014