Jawaban Eran menjelaskan perbedaan antara versi dua-argumen dan tiga-argumen reduce
dalam yang pertama direduksi Stream<T>
menjadi T
sedangkan yang terakhir dikurangi Stream<T>
menjadi U
. Namun, itu tidak benar-benar menjelaskan perlunya fungsi penggabung tambahan saat mengurangi Stream<T>
ke U
.
Salah satu prinsip desain Streams API adalah bahwa API tidak boleh berbeda antara aliran sekuensial dan paralel, atau dengan kata lain, API tertentu tidak boleh mencegah aliran berjalan dengan benar baik secara berurutan atau paralel. Jika lambda Anda memiliki properti yang benar (asosiatif, tidak mengganggu, dll.), Aliran yang dijalankan secara berurutan atau paralel akan memberikan hasil yang sama.
Mari pertama-tama pertimbangkan versi reduksi dua argumen:
T reduce(I, (T, T) -> T)
Implementasi sekuensial sangat mudah. Nilai identitas I
"diakumulasikan" dengan elemen aliran nol untuk memberikan hasil. Hasil ini diakumulasikan dengan elemen aliran pertama untuk memberikan hasil lain, yang pada gilirannya diakumulasikan dengan elemen aliran kedua, dan seterusnya. Setelah elemen terakhir diakumulasikan, hasil akhirnya dikembalikan.
Penerapan paralel dimulai dengan membagi aliran menjadi beberapa segmen. Setiap segmen diproses oleh utasnya sendiri secara berurutan yang saya jelaskan di atas. Sekarang, jika kami memiliki N utas, kami memiliki hasil menengah N. Ini perlu dikurangi menjadi satu hasil. Karena setiap hasil perantara berjenis T, dan kita memiliki beberapa, kita dapat menggunakan fungsi akumulator yang sama untuk mengurangi hasil perantara N tersebut menjadi satu hasil.
Sekarang mari kita pertimbangkan operasi pengurangan dua argumen hipotetis yang direduksi Stream<T>
menjadi U
. Dalam bahasa lain, ini disebut operasi "lipat" atau "lipat kiri" jadi saya akan menyebutnya di sini. Perhatikan bahwa ini tidak ada di Java.
U foldLeft(I, (U, T) -> U)
(Perhatikan bahwa nilai identitas I
adalah tipe U.)
Versi sekuensial dari foldLeft
sama seperti versi sekuensial reduce
kecuali bahwa nilai tengahnya adalah tipe U dan bukan tipe T. Tapi sebaliknya sama. ( foldRight
Operasi hipotetis akan serupa kecuali bahwa operasi akan dilakukan dari kanan ke kiri, bukan dari kiri ke kanan.)
Sekarang perhatikan versi paralel dari foldLeft
. Mari kita mulai dengan membagi arus menjadi beberapa segmen. Kemudian kita dapat meminta setiap benang N mengurangi nilai T di segmennya menjadi nilai antara N tipe U. Sekarang apa? Bagaimana kita mendapatkan dari nilai N tipe U ke hasil tunggal tipe U?
Apa yang hilang adalah fungsi lain yang menggabungkan beberapa hasil antara tipe U menjadi satu hasil tipe U. Jika kita memiliki fungsi yang menggabungkan dua nilai U menjadi satu, itu cukup untuk mengurangi sejumlah nilai menjadi satu - seperti pengurangan asli di atas. Dengan demikian, operasi reduksi yang memberikan hasil dari tipe yang berbeda membutuhkan dua fungsi:
U reduce(I, (U, T) -> U, (U, U) -> U)
Atau, menggunakan sintaks Java:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Singkatnya, untuk melakukan reduksi paralel ke jenis hasil yang berbeda, kita memerlukan dua fungsi: satu yang mengakumulasi elemen T ke nilai U perantara, dan yang kedua yang menggabungkan nilai U perantara menjadi satu hasil U. Jika kita tidak berpindah tipe, ternyata fungsi akumulatornya sama dengan fungsi penggabung. Itulah mengapa reduksi ke jenis yang sama hanya memiliki fungsi akumulator dan reduksi ke jenis yang berbeda memerlukan fungsi akumulator dan penggabung yang terpisah.
Akhirnya, Java tidak menyediakan foldLeft
dan foldRight
operasi karena mereka menyiratkan pemesanan tertentu operasi yang secara inheren berurutan. Ini bertentangan dengan prinsip desain yang disebutkan di atas dalam menyediakan API yang mendukung operasi sekuensial dan paralel secara setara.