Iterable and Sequence Kotlin terlihat persis sama. Mengapa dua jenis diperlukan?

88

Kedua antarmuka ini hanya mendefinisikan satu metode

public operator fun iterator(): Iterator<T>

Dokumentasi mengatakan Sequencedimaksudkan untuk menjadi malas. Tapi bukankah Iterablemalas juga (kecuali didukung oleh a Collection)?

Venkata Raju
sumber

Jawaban:

138

Perbedaan utama terletak pada semantik dan implementasi fungsi ekstensi stdlib untuk Iterable<T>dan Sequence<T>.

  • Sebab Sequence<T>, fungsi ekstensi berjalan lambat jika memungkinkan, mirip dengan operasi perantara Java Streams . Misalnya, Sequence<T>.map { ... }mengembalikan yang lain Sequence<R>dan tidak benar-benar memproses item hingga operasi terminal seperti toListatau folddipanggil.

    Pertimbangkan kode ini:

    val seq = sequenceOf(1, 2)
    val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate
    print("before sum ")
    val sum = seqMapped.sum() // terminal
    

    Ini mencetak:

    before sum 1 2
    

    Sequence<T>ditujukan untuk penggunaan yang lambat dan pipelining yang efisien ketika Anda ingin mengurangi pekerjaan yang dilakukan dalam operasi terminal sebanyak mungkin, sama dengan Java Streams. Namun, kemalasan menyebabkan beberapa overhead, yang tidak diinginkan untuk transformasi sederhana yang umum dari koleksi yang lebih kecil dan membuatnya kurang berkinerja.

    Secara umum, tidak ada cara yang baik untuk menentukan kapan dibutuhkan, jadi di Kotlin stdlib kemalasan dibuat eksplisit dan diekstraksi ke Sequence<T>antarmuka untuk menghindari penggunaannya di semua Iterablesecara default.

  • Karena Iterable<T>, sebaliknya, fungsi ekstensi dengan semantik operasi perantara bekerja dengan penuh semangat, memproses item segera dan mengembalikan yang lain Iterable. Misalnya, Iterable<T>.map { ... }mengembalikan a List<R>dengan hasil pemetaan di dalamnya.

    Kode yang setara untuk Iterable:

    val lst = listOf(1, 2)
    val lstMapped: List<Int> = lst.map { print("$it "); it * it }
    print("before sum ")
    val sum = lstMapped.sum()
    

    Ini mencetak:

    1 2 before sum
    

    Seperti yang dikatakan di atas, Iterable<T>tidak malas secara default, dan solusi ini menunjukkan dirinya dengan baik: dalam banyak kasus ia memiliki lokalitas referensi yang baik sehingga memanfaatkan cache CPU, prediksi, prefetching, dll. Sehingga bahkan beberapa penyalinan koleksi masih berfungsi dengan baik cukup dan berkinerja lebih baik dalam kasus sederhana dengan koleksi kecil.

    Jika Anda membutuhkan lebih banyak kontrol atas pipeline evaluasi, ada konversi eksplisit ke urutan malas dengan Iterable<T>.asSequence()fungsi.

hotkey
sumber
3
Mungkin kejutan besar bagi Java(kebanyakan Guava) penggemar
Venkata Raju
@VenkataRaju untuk orang-orang fungsional, mereka mungkin terkejut dengan alternatif lazy secara default.
Jayson Minard
9
Lazy secara default biasanya kurang berkinerja untuk koleksi yang lebih kecil dan lebih umum digunakan. Salinan bisa lebih cepat daripada evaluasi malas jika memanfaatkan cache CPU dan seterusnya. Jadi untuk kasus penggunaan umum lebih baik tidak malas. Dan sayangnya kontrak umum untuk fungsi seperti map, filterdan lainnya tidak membawa cukup informasi untuk memutuskan selain dari jenis kumpulan sumber, dan karena sebagian besar koleksi juga dapat diulang, itu bukan penanda yang baik untuk "malas" karena itu biasanya DI MANA SAJA. malas harus eksplisit agar aman.
Jayson Minard
1
@naki Salah satu contoh dari pengumuman Apache Spark baru-baru ini, jelas mereka mengkhawatirkan hal ini, lihat bagian "Perhitungan yang sadar-cache" di databricks.com/blog/2015/04/28/… ... tetapi mereka mengkhawatirkan miliaran hal-hal yang berulang sehingga mereka harus sepenuhnya ekstrem.
Jayson Minard
3
Selain itu, kesalahan umum dengan evaluasi malas adalah menangkap konteks dan menyimpan penghitungan lambat yang dihasilkan di bidang bersama dengan semua penduduk setempat yang ditangkap dan apa pun yang mereka pegang. Karenanya, sulit untuk men-debug kebocoran memori.
Ilya Ryzhenkov
50

Menyelesaikan jawaban hotkey:

Penting untuk memperhatikan bagaimana Sequence dan Iterable mengulangi seluruh elemen Anda:

Contoh urutan:

list.asSequence().filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

Hasil log:

filter - Peta - Masing-masing; filter - Peta - Masing-masing

Contoh Iterable:

list.filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

filter - filter - Peta - Peta - Masing - Masing

Leandro Borges Ferreira
sumber
5
Itu adalah contoh yang sangat bagus dari perbedaan antara keduanya.
Alexey Soshin
Ini adalah contoh yang bagus.
frye3k
2

Iterabledipetakan ke java.lang.Iterableantarmuka di JVM, dan diimplementasikan oleh koleksi yang umum digunakan, seperti List atau Set. Fungsi ekstensi koleksi ini dievaluasi dengan penuh semangat, yang berarti mereka semua segera memproses semua elemen dalam masukannya dan mengembalikan koleksi baru yang berisi hasilnya.

Berikut adalah contoh sederhana penggunaan fungsi collection untuk mendapatkan nama dari lima orang pertama dalam daftar yang usianya setidaknya 21 tahun:

val people: List<Person> = getPeople()
val allowedEntrance = people
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)

Platform target: JVMRunning di kotlin v. 1.3.61 Pertama, pemeriksaan usia dilakukan untuk setiap Orang dalam daftar, dengan hasil dimasukkan ke dalam daftar baru. Kemudian, pemetaan ke nama mereka dilakukan untuk setiap Orang yang tetap setelah operator filter, berakhir di daftar baru lainnya (ini sekarang a List<String>). Terakhir, ada satu daftar baru yang dibuat untuk memuat lima elemen pertama dari daftar sebelumnya.

Sebaliknya, Sequence adalah konsep baru di Kotlin yang merepresentasikan kumpulan nilai yang dievaluasi dengan lambat. Ekstensi koleksi yang sama tersedia untuk Sequenceantarmuka, tetapi ini segera mengembalikan instance Urutan yang mewakili status tanggal yang diproses, tetapi tanpa benar-benar memproses elemen apa pun. Untuk memulai pemrosesan, Sequenceharus diakhiri dengan operator terminal, ini pada dasarnya adalah permintaan ke Urutan untuk mewujudkan data yang diwakilinya dalam beberapa bentuk konkret. Contohnya termasuk toList,, toSetdan sum, untuk menyebutkan beberapa. Ketika ini dipanggil, hanya jumlah minimum elemen yang dibutuhkan yang akan diproses untuk menghasilkan hasil yang diminta.

Mengubah koleksi yang ada menjadi Urutan sangatlah mudah, Anda hanya perlu menggunakan asSequenceekstensi. Seperti disebutkan di atas, Anda juga perlu menambahkan operator terminal, jika tidak, Urutan tidak akan pernah melakukan pemrosesan apa pun (sekali lagi, malas!).

val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)
    .toList()

Platform target: JVMRunning di kotlin v. 1.3.61 Dalam kasus ini, instance Person dalam Urutan masing-masing diperiksa usianya, jika lolos, namanya diekstrak, lalu ditambahkan ke daftar hasil. Ini diulangi untuk setiap orang dalam daftar awal hingga ada lima orang yang ditemukan. Pada titik ini, fungsi toList mengembalikan daftar, dan orang lain di dalam Sequencetidak diproses.

Ada juga sesuatu yang ekstra yang mampu dilakukan oleh Urutan: dapat berisi item dalam jumlah tak terbatas. Dengan perspektif ini, masuk akal bahwa operator bekerja dengan cara yang mereka lakukan - operator pada urutan yang tidak terbatas tidak akan pernah bisa kembali jika melakukan pekerjaannya dengan penuh semangat.

Sebagai contoh, berikut adalah urutan yang akan menghasilkan kekuatan 2 sebanyak yang dibutuhkan oleh operator terminalnya (mengabaikan fakta bahwa ini akan cepat meluap):

generateSequence(1) { n -> n * 2 }
    .take(20)
    .forEach(::println)

Anda dapat menemukan lebih banyak di sini .

Sazzad Hissain Khan
sumber