Mengapa diperlukan penggabung untuk metode reduksi yang mengubah tipe di java 8

150

Saya kesulitan memahami sepenuhnya peran yang combinerdipenuhi dalam reducemetode Streams .

Misalnya, kode berikut tidak dapat dikompilasi:

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

Kesalahan kompilasi mengatakan: (argumen mismatch; int tidak dapat dikonversi ke java.lang.String)

tetapi kode ini mengkompilasi:

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

Saya memahami bahwa metode penggabung digunakan dalam aliran paralel - jadi dalam contoh saya itu menambahkan bersama dua int terakumulasi menengah.

Tapi saya tidak mengerti mengapa contoh pertama tidak dapat dikompilasi tanpa penggabung atau bagaimana penggabung menyelesaikan konversi string ke int karena ini hanya menambahkan bersama dua int.

Adakah yang bisa menjelaskan ini?

Louise Miller
sumber
Pertanyaan terkait: stackoverflow.com/questions/24202473/…
nosid
2
aha, ini untuk aliran paralel ... Saya sebut abstraksi bocor!
Andy

Jawaban:

78

Dua dan tiga versi argumen reduceyang Anda coba gunakan tidak menerima tipe yang sama untuk accumulator.

Kedua argumen reducetersebut didefinisikan sebagai :

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

Dalam kasus Anda, T adalah String, jadi BinaryOperator<T>harus menerima dua argumen String dan mengembalikan String. Tapi Anda meneruskan ke int dan String, yang menghasilkan kesalahan kompilasi yang Anda dapatkan - argument mismatch; int cannot be converted to java.lang.String. Sebenarnya, saya pikir melewatkan 0 sebagai nilai identitas juga salah di sini, karena String diharapkan (T).

Juga perhatikan bahwa versi mengurangi proses aliran Ts dan mengembalikan T, jadi Anda tidak dapat menggunakannya untuk mengurangi aliran String ke int.

Ketiga argumen reducetersebut didefinisikan sebagai :

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

Dalam kasus Anda U adalah Integer dan T adalah String, jadi metode ini akan mengurangi aliran String menjadi Integer.

Untuk BiFunction<U,? super T,U>akumulator, Anda dapat mengirimkan parameter dari dua tipe berbeda (U dan? Super T), yang dalam kasus Anda adalah Integer dan String. Selain itu, nilai identitas U menerima Integer dalam kasus Anda, jadi meneruskannya dengan 0 tidak masalah.

Cara lain untuk mencapai apa yang Anda inginkan:

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

Di sini jenis aliran cocok dengan jenis kembalian reduce, sehingga Anda dapat menggunakan dua versi parameter reduce.

Tentu saja Anda tidak harus menggunakan reducesama sekali:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();
Eran
sumber
9
Sebagai opsi kedua dalam kode terakhir Anda, Anda juga dapat menggunakan mapToInt(String::length)lebih mapToInt(s -> s.length()), tidak yakin apakah salah satu lebih baik daripada yang lain, tetapi saya lebih suka yang pertama untuk keterbacaan.
skiwi
24
Banyak yang akan menemukan jawaban ini karena mereka tidak mengerti mengapa combinerdiperlukan, mengapa tidak memiliki accumulatorsudah cukup. Dalam hal ini: Penggabung hanya diperlukan untuk aliran paralel, untuk menggabungkan hasil "akumulasi" dari utas.
ddekany
2
Menurut saya jawaban Anda tidak berguna - karena Anda sama sekali tidak menjelaskan apa yang harus dilakukan penggabung dan bagaimana saya dapat bekerja tanpanya! Dalam kasus saya, saya ingin mengurangi tipe T menjadi U tetapi tidak ada cara ini bisa dilakukan secara paralel sama sekali. Itu tidak mungkin. Bagaimana Anda memberi tahu sistem bahwa saya tidak ingin / membutuhkan paralelisme dan dengan demikian mengabaikan penggabung?
Zordid
@Zordid Stream API tidak menyertakan opsi untuk mengurangi tipe T menjadi U tanpa melewatkan penggabung.
Eran
Jawaban ini sama sekali tidak menjelaskan penggabung, hanya mengapa OP membutuhkan varian non-penggabung.
Benny Bottema
226

Jawaban Eran menjelaskan perbedaan antara versi dua-argumen dan tiga-argumen reducedalam yang pertama direduksi Stream<T>menjadi Tsedangkan 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 Iadalah tipe U.)

Versi sekuensial dari foldLeftsama seperti versi sekuensial reducekecuali bahwa nilai tengahnya adalah tipe U dan bukan tipe T. Tapi sebaliknya sama. ( foldRightOperasi 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 foldLeftdan foldRightoperasi 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.

Stuart Marks
sumber
9
Jadi apa yang dapat Anda lakukan jika Anda memerlukan foldLeftkarena perhitungannya bergantung pada hasil sebelumnya dan tidak dapat diparalelkan?
amoebe
5
@amoebe Anda dapat mengimplementasikan foldLeft Anda sendiri menggunakan forEachOrdered. Status perantara harus disimpan dalam variabel yang ditangkap.
Stuart Marks
@StuartMarks terima kasih, saya akhirnya menggunakan jOOλ. Mereka memiliki implementasi yangfoldLeft rapi .
amoebe
1
Suka jawaban ini! Koreksi saya jika saya salah: ini menjelaskan mengapa contoh OP yang berjalan (yang kedua) tidak akan pernah memanggil penggabung, ketika dijalankan, menjadi aliran sekuensial.
Luigi Cortese
2
Ini menjelaskan hampir semuanya ... kecuali: mengapa ini harus mengecualikan reduksi berbasis berurutan. Dalam kasus saya, TIDAK MUNGKIN melakukannya secara paralel karena pengurangan saya mengurangi daftar fungsi menjadi U dengan memanggil setiap fungsi pada hasil perantara dari hasil pendahulunya. Ini tidak dapat dilakukan secara paralel sama sekali dan tidak ada cara untuk menggambarkan penggabung. Metode apa yang dapat saya gunakan untuk melakukannya?
Zordid
124

Karena saya suka corat-coret dan panah untuk mengklarifikasi konsep ... ayo mulai!

Dari String ke String (aliran berurutan)

Misalkan memiliki 4 string: tujuan Anda adalah menggabungkan string tersebut menjadi satu. Anda pada dasarnya memulai dengan satu tipe dan menyelesaikannya dengan tipe yang sama.

Anda dapat mencapai ini dengan

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

dan ini membantu Anda memvisualisasikan apa yang terjadi:

masukkan deskripsi gambar di sini

Fungsi akumulator mengubah, selangkah demi selangkah, elemen dalam aliran (merah) Anda menjadi nilai akhir yang dikurangi (hijau). Fungsi akumulator hanya mengubah suatu Stringobjek menjadi objek lain String.

Dari String ke int (aliran paralel)

Misalkan memiliki 4 string yang sama: tujuan baru Anda adalah menjumlahkan panjangnya, dan Anda ingin memparalelkan aliran Anda.

Yang Anda butuhkan adalah sesuatu seperti ini:

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

dan ini adalah skema dari apa yang terjadi

masukkan deskripsi gambar di sini

Di sini, fungsi akumulator (a BiFunction) memungkinkan Anda mengubah Stringdata menjadi intdata. Menjadi aliran paralel, itu terbagi menjadi dua bagian (merah), yang masing-masing diuraikan secara independen satu sama lain dan menghasilkan hasil parsial (oranye) yang sama banyaknya. Mendefinisikan penggabung diperlukan untuk memberikan aturan untuk menggabungkan inthasil parsial menjadi hasil akhir (hijau) int.

Dari String ke int (aliran berurutan)

Bagaimana jika Anda tidak ingin memparalelkan aliran Anda? Yah, bagaimanapun, penggabung perlu disediakan, tetapi itu tidak akan pernah dipanggil, mengingat tidak ada hasil parsial yang akan dihasilkan.

Luigi Cortese
sumber
8
Terima kasih untuk ini. Saya bahkan tidak perlu membaca. Saya berharap mereka baru saja menambahkan fungsi lipatan yang aneh.
Lodewijk Bogaards
1
@LodewijkBogaards senang itu membantu! JavaDoc di sini memang cukup samar
Luigi Cortese
@LuigiCortese Dalam aliran paralel apakah selalu membagi elemen untuk berpasangan?
TheLogicGuy
2
Saya menghargai jawaban Anda yang jelas dan berguna. Saya ingin mengulangi sedikit dari apa yang Anda katakan: "Ya, penggabung perlu disediakan, tetapi tidak akan pernah dipanggil." Ini adalah bagian dari pemrograman fungsional Brave New World of Java yang, saya yakin berkali-kali, "membuat kode Anda lebih ringkas dan lebih mudah dibaca." Mari berharap bahwa contoh kejelasan singkat (kutipan jari) seperti ini tetap sedikit dan jarang.
dnuttle
Akan JAUH lebih baik untuk menggambarkan pengurangan dengan delapan senar ...
Ekaterina Ivanova iceja.net
0

Tidak ada versi pengurangan yang mengambil dua jenis berbeda tanpa penggabung karena tidak dapat dijalankan secara paralel (tidak yakin mengapa ini menjadi persyaratan). Fakta bahwa akumulator harus asosiatif membuat antarmuka ini tidak berguna karena:

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

Menghasilkan hasil yang sama seperti:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);
quiz123
sumber
mapTrik seperti itu tergantung pada khususnya accumulatordan combinerdapat memperlambat banyak hal.
Tagir Valeev
Atau, percepat secara signifikan karena Anda sekarang dapat menyederhanakannya accumulatordengan menghapus parameter pertama.
quiz123
Reduksi paralel dimungkinkan, itu tergantung pada perhitungan Anda. Dalam kasus Anda, Anda harus menyadari kerumitan penggabung tetapi juga akumulator pada instance identitas vs instance lainnya.
LoganMzz