Iterable lazy

Iterable yang lazy: transformasi berantai yang dievaluasi saat dibutuhkan.

Daftar isi

Iterable pada Dart bersifat lazy, yaitu transformasi berantainya baru dijalankan pada saat elemennya benar-benar dibutuhkan. Perilaku ini berbeda dengan List yang menyimpan elemen yang sudah terhitung (eager). Topik ini membahas cara kerja evaluasi lambat tersebut, perbedaannya dengan List, materialisasi hasil melalui toList(), serta Iterable.generate untuk membuat deret yang elemennya dihitung saat dibaca. Jejak print disisipkan di antara transformasi agar jalannya evaluasi terlihat secara langsung dan tetap deterministik.

List yang eager dan Iterable yang lazy#

List menyimpan elemen yang sudah ada di memori, sehingga pembacaan elemennya selesai seketika. Sebagian besar operasi yang mengembalikan koleksi baru, seperti map dan where, tidak mengembalikan List, melainkan Iterable yang hanya menyimpan deskripsi transformasinya. Deskripsi itu baru dijalankan pada saat elemen diminta, baik melalui perulangan for-in, pembacaan first, maupun konversi toList(). Istilah lazy digunakan untuk perilaku ini, sedangkan eager menyatakan kebalikannya, yaitu pekerjaan selesai pada saat operasi dideklarasikan. Perbedaannya tidak mengubah hasil akhir, tetapi mengubah waktu dan banyaknya pekerjaan yang benar-benar dijalankan.

Evaluasi lambat juga menentukan urutan kerja di dalam rantai transformasi. Ekspresi seperti nilai.where(...).map(...) tidak menjalankan where untuk seluruh elemen terlebih dahulu, melainkan menarik elemen satu per satu sepanjang rantai selama iterasi berlangsung. Elemen yang ditolak oleh where tidak pernah mencapai tahap berikutnya, sehingga pekerjaan yang tidak perlu terpangkas secara otomatis. Urutan penyusunan rantai tetap berpengaruh, karena transformasi yang lebih murah sebaiknya diletakkan lebih dekat ke sumber data.

Salah satu cara yang meyakinkan untuk melihat perbedaannya adalah menyisipkan print di dalam fungsi transformasi, sehingga setiap eksekusi fungsi itu meninggalkan jejak yang terbaca pada keluaran. Contoh berikut memetakan tiga nilai menjadi proporsi, lalu hanya membaca elemen pertamanya. Dengan demikian, jejak yang muncul pada keluaran menggambarkan secara persis kapan pekerjaan pemetaan benar-benar dijalankan.

dart jejak-lazy.dart
void main() {
  var nilai = [80, 65, 90];
  print('Sebelum membuat transformasi');
  var proporsi = nilai.map((n) {
    print('memetakan nilai $n');
    return n / 100;
  });
  print('Transformasi dibuat, belum ada jejak pemetaan');
  print('Elemen pertama: ${proporsi.first}');
  print('Selesai');
}
text Keluaran
Sebelum membuat transformasi
Transformasi dibuat, belum ada jejak pemetaan
memetakan nilai 80
Elemen pertama: 0.8
Selesai

Pesan Sebelum membuat transformasi dicetak sebelum map dipanggil, sedangkan jejak memetakan nilai 80 baru muncul ketika first membaca satu elemen. Elemen kedua dan ketiga tidak pernah dipetakan karena tidak pernah diminta. Perilaku ini menegaskan bahwa pemanggilan map hanyalah pembuatan deskripsi transformasi, dan pekerjaan sesungguhnya terjadi selama iterasi. Apabila seluruh elemen dibaca dengan for-in, ketiga jejak pemetaannya akan muncul berurutan. Evaluasi lambat bukanlah penghematan yang dilakukan diam-diam oleh kompiler, melainkan kontrak yang ditetapkan dokumentasi Iterable itu sendiri.

Materialisasi dengan toList#

Pemanggilan toList() menjalankan seluruh transformasi dan menyimpan hasilnya ke dalam List baru, sebuah proses yang disebut materialisasi (materialization). Setelah dimaterialisasi, hasilnya sudah terhitung dan aman dibaca berulang kali tanpa menjalankan transformasinya lagi. Sebaliknya, Iterable yang belum dimaterialisasi akan menjalankan transformasinya setiap kali diiterasi, sehingga membaca dua kali berarti menghitung dua kali. Konsekuensinya menjadi penting pada transformasi yang berat, karena lupa materialisasi dapat menggandakan pekerjaan tanpa terlihat dari hasil akhirnya. Perhatikan pula bahwa properti length pada Iterable yang lazy mengiterasi seluruh elemennya, sehingga membaca length setara dengan menjalankan transformasi sepenuhnya.

Contoh berikut memanggil toList() dua kali pada Iterable yang sama dan mencetak jejak pemetaannya di antara keduanya.

dart daftar-ulang.dart
void main() {
  var nilai = [80, 65, 90];
  var proporsi = nilai.map((n) {
    print('memetakan nilai $n');
    return n / 100;
  });

  var daftarPertama = proporsi.toList();
  print('Daftar pertama: $daftarPertama');

  var daftarKedua = proporsi.toList();
  print('Daftar kedua: $daftarKedua');
}
text Keluaran
memetakan nilai 80
memetakan nilai 65
memetakan nilai 90
Daftar pertama: [0.8, 0.65, 0.9]
memetakan nilai 80
memetakan nilai 65
memetakan nilai 90
Daftar kedua: [0.8, 0.65, 0.9]

Keluaran memperlihatkan enam jejak pemetaan untuk tiga elemen, karena setiap pemanggilan toList() menjalankan kembali transformasinya dari awal. Kedua daftar berisi nilai yang sama, tetapi pekerjaannya dijalankan dua kali. Sebaliknya, pembacaan berulang atas daftarPertama tidak menjalankan pemetaan lagi karena isinya sudah terhitung dan tersimpan sebagai List. Kebiasaan yang aman adalah mematerialisasi satu kali pada titik yang membutuhkan hasilnya, lalu menyimpan List hasilnya untuk dibaca berkali-kali.

Iterable.generate#

Iterable.generate membuat deret dari sebuah fungsi yang menerima indeks, dan elemennya baru dihitung saat diiterasi. Bentuk pemanggilannya adalah Iterable.generate(jumlah, fungsi), dengan jumlah menyatakan banyaknya elemen yang dijanjikan dan fungsi menyusun nilai untuk setiap indeksnya. Fungsi per indeks tidak dipanggil pada saat deret dibuat, melainkan setiap kali elemen pada indeks itu diminta. Pola ini cocok untuk deret yang rumusnya jelas tetapi penghitungannya belum tentu diperlukan semuanya, misalnya deret pembacaan sensor tiruan untuk uji coba.

Contoh berikut membuat tiga pembacaan suhu tiruan, lalu mengiterasinya dengan for-in untuk memverifikasi kapan penghitungan terjadi.

dart generator-suhu.dart
void main() {
  var pembacaan = Iterable.generate(3, (i) {
    print('menghitung pembacaan ke-$i');
    return 24.0 + i * 0.5;
  });
  print('Generator dibuat');

  for (var suhu in pembacaan) {
    print('Suhu: $suhu');
  }
}
text Keluaran
Generator dibuat
menghitung pembacaan ke-0
Suhu: 24.0
menghitung pembacaan ke-1
Suhu: 24.5
menghitung pembacaan ke-2
Suhu: 25.0

Pesan Generator dibuat muncul sebelum ada satu pun fungsi per indeks yang dijalankan, dan penghitungan tiap pembacaan baru terjadi ketika for-in menemukan elemennya. Urutan keluaran menunjukkan pola bergantian antara penghitungan dan pencetakan, yang menegaskan bahwa elemen dihitung satu per satu selama iterasi berlangsung. Apabila perulangan dihentikan lebih awal dengan break, elemen setelah titik henti tidak akan pernah dihitung. Perilaku inilah yang membedakan deret generator dari List yang isinya sudah siap sejak awal. Deret generator juga dapat menjadi sumber map atau where, karena kedua metode itu menerima Iterable apa pun, dan hasil gabungannya tetap lazy sampai dimaterialisasi.

Ringkasan#

  • map dan where mengembalikan Iterable yang menyimpan deskripsi transformasi, dan elemennya baru dihitung saat diminta.
  • Evaluasi lambat membuat pekerjaan hanya berjalan untuk elemen yang benar-benar dibaca, misalnya first hanya memetakan satu elemen.
  • toList() mematerialisasi hasil menjadi List, sedangkan Iterable yang belum dimaterialisasi mengulang transformasinya setiap kali diiterasi.
  • Iterable.generate membuat deret dari fungsi per indeks yang baru dijalankan saat elemennya dibaca.

Referensi#