Ada satu kejutan yang hampir semua pemula Rust alami saat pertama kali memakai iterator: kode transformasi yang sudah ditulis ternyata tidak pernah berjalan. Kita membangun pipeline pengolahan data, program berakhir tanpa pesan galat apa pun, dan hasilnya… tidak ada sama sekali — bukan karena rusak, melainkan karena iterator di Rust bersifat lazy: iterator tidak memproses satu elemen pun sampai ada pihak lain yang meminta hasilnya secara eksplisit. Pihak peminta tersebut bernama consumer — perulangan for, collect, sum, dan kawan-kawannya. Pada chapter ini kita buktikan sifat malas ini dengan program sungguhan yang outputnya bisa kita amati, karena memahami kapan iterator bekerja adalah kunci menghindari bug “kenapa kode saya tidak dieksekusi” yang sangat umum.
Pipeline yang Tidak Bekerja #
Mari kita mulai dari kasus yang tampak wajar. Kode berikut membuat pipeline yang menggandakan setiap harga: iterator dibuat dari prices, lalu method map — adapter yang menjalankan sebuah closure pada setiap elemen — menerimanya. Di dalam closure kita sengaja menaruh println! sebagai penanda bahwa closure tersebut benar-benar dieksekusi:
fn main() {
let prices = vec![15000, 17500, 20000];
let _pipeline = prices.iter().map(|price| {
println!("Memproses harga: Rp{}", price);
price * 2
});
println!("Pipeline sudah dibuat, program selesai tanpa memproses apa pun");
}Output yang Dihasilkan:
Pipeline sudah dibuat, program selesai tanpa memproses apa punOutput ini adalah bukti keras sifat lazy: Memproses harga: Rp15000 tidak pernah tercetak. Mari kita bedah mengapa:
- Baris 4-7 (
let _pipeline = prices.iter().map(|price| { ... });): ekspresi ini hanya membangun pipeline.prices.iter()membuat iterator sumber yang belum membaca apa pun, dan.map(...)membungkusnya menjadi iterator baru yang menyimpan closure untuk dipakai nanti. Tidak ada satu pun elemen yang disentuh pada baris-baris ini. - Baris 5 (
println!("Memproses harga: Rp{}", price);): baris ini adalah isi closure — kode yang akan berjalan jika dan hanya jika consumer menarik elemen. Selama tidak ada penarikan, baris ini tidak pernah dieksekusi. - Baris 9 (
println!("Pipeline sudah dibuat, ...");): satu-satunya baris yang benar-benar berjalan. Program kemudian selesai, pipeline dijatuhkan tanpa pernah bekerja, dan closure besertaprintln!di dalamnya mati tanpa jejak.
Bandingkan dengan intuisi dari bahasa lain: di PHP atau Python, memanggil fungsi transformasi biasanya langsung menjalankan seluruh proses dan mengembalikan hasil jadi. Iterator Rust menunda seluruh pekerjaan sampai ada yang menagih. Penanda _ di depan nama variabel sengaja kita pakai untuk menyatakan bahwa pipeline sengaja tidak dikonsumsi.
Consumer sebagai Pemicu Kerja #
Sekarang kita tambahkan consumer: perulangan for yang kita kenal sebagai driver next dari dua chapter sebelumnya. Perhatikan urutan baris output dengan saksama:
fn main() {
let prices = vec![15000, 17500, 20000];
let pipeline = prices.iter().map(|price| {
println!("Memproses harga: Rp{}", price);
price * 2
});
println!("Sebelum consumer dijalankan");
for doubled in pipeline {
println!("Hasil akhir: Rp{}", doubled);
}
}Output yang Dihasilkan:
Sebelum consumer dijalankan
Memproses harga: Rp15000
Hasil akhir: Rp30000
Memproses harga: Rp17500
Hasil akhir: Rp35000
Memproses harga: Rp20000
Hasil akhir: Rp40000Uraian baris-per-baris:
- Baris 4-7 (
let pipeline = ...): pembangunan pipeline, sama seperti contoh pertama. Sampai titik ini masih tidak ada pemrosesan — closure hanya disimpan. - Baris 9 (
println!("Sebelum consumer dijalankan");): baris ini tercetak duluan, membuktikan bahwa baris 4-7 tidak menjalankan apa pun. - Baris 11 (
for doubled in pipeline {):formulai memanggilnextpada pipeline. Setiap panggilannextpada iteratormapmenarik satu elemen dari iterator sumber, menjalankan closure tepat satu kali, lalu mengembalikan hasilnya. - Baris 12 (
println!("Hasil akhir: Rp{}", doubled);): badan perulangan menerima hasil closure.
Output memberitahu kisah yang menarik: Memproses harga: Rp15000 dan Hasil akhir: Rp30000 muncul berselang-seling, bukan semua Memproses duluan lalu semua Hasil. Urutan berselang ini membuktikan bahwa elemen mengalir satu per satu — closure dieksekusi untuk elemen pertama, hasilnya dicetak, baru closure dieksekusi untuk elemen kedua. Tidak ada tahap “proses semua dulu, lalu cetak semua”, karena memang tidak pernah ada koleksi hasil sementara yang dibuat.
Model Tarik-menarik di Balik Layar #
Mekanisme di balik perilaku ini disebut pull model: data tidak didorong dari sumber ke tujuan, melainkan ditarik dari ujung consumer, tahap demi tahap ke belakang.
Diagram di atas memperlihatkan satu siklus hidup satu elemen: for memanggil next pada map, map merespons dengan memanggil next pada sumber, sumber menyerahkan Some(&15000), closure mengolahnya, dan hasil Some(30000) mengalir kembali ke consumer. Elemen berikutnya tidak akan disentuh sebelum for menagih lagi — dan seluruh aliran berhenti begitu sumber mengembalikan None. Model tarik inilah yang menjelaskan mengapa pipeline hanya “hidup” ketika ada yang menarik.
Mengapa Lazy Menang #
Kemalasan ini bukan kemalasan pemalas, melainkan desain yang membayar tiga keuntungan konkret:
- Komposisi tanpa biaya perantara. Rangkaian
filterlalumaplalumaplagi dapat kita susun bertingkat tanpa satu punVecsementara. Setiap elemen mengalir melewati seluruh pipeline dalam satu tarikan, bukan disimpan di koleksi antara di setiap tahap. Kita akan merasakan manfaat ini penuh pada chapter Menggabungkan Adapter. - Pekerjaan hanya untuk elemen yang dibutuhkan. Consumer yang berhenti di tengah — misalnya hanya mengambil beberapa elemen pertama — membuat sisa data tidak pernah diproses sama sekali.
- Kesempatan optimasi. Kompilator melihat pipeline sebagai rangkaian panggilan
nextsederhana yang mudah dioptimasi menjadi loop tanpa overhead pemanggilan fungsi.
Ringkasan #
Iterator Rust dievaluasi secara lazy: membangun pipeline dengan map tidak menjalankan closure sedikit pun, dan seluruh pekerjaan baru dimulai ketika consumer seperti for menarik elemen lewat next. Output yang berselang antara Memproses dan Hasil akhir membuktikan aliran elemen satu-per-satu mengikuti model tarik dari consumer ke sumber, tanpa koleksi perantara. Konsekuensi praktisnya ada dua: pipeline tanpa consumer adalah kode mati yang tidak dianggap galat oleh kompilator, dan pipeline dengan consumer adalah mesin hemat yang hanya memproses apa yang ditagih. Dengan aturan main ini jelas, kita siap membedah adapter pertama dan paling sering dipakai: map.