Laziness menyederhanakan deklarasi, tetapi perilaku eksekusinya mengejutkan bila mekanismenya tidak dipahami. Topik ini membahas jebakan yang umum terjadi, yaitu menahan head hingga memori terbebani, chunking yang merealisasikan lebih banyak elemen daripada yang diminta, efek samping yang berjalan terlambat, dan fungsi yang tidak pernah selesai atas seq tak terbatas.
Menahan head#
Head adalah rujukan ke elemen pertama sebuah seq. Lazy sequence yang sudah sebagian direalisasikan menyimpan hasil perhitungannya selama masih ada rujukan ke head tersebut. Mengikat hasil map pada data besar dengan def berarti menahan head, sehingga seluruh seq yang telah direalisasikan tetap berada di memori selama var tersebut masih menunjuk head itu.
Contoh kecil berikut menunjukkan efeknya secara terukur.
(def panggilan (atom 0))
(def data (map (fn [x] (swap! panggilan inc) (* x x)) (range 5)))
(println "sebelum:" @panggilan)
(println "reduksi pertama:" (reduce + data))
(println "setelah reduksi pertama:" @panggilan)
(println "reduksi kedua:" (reduce + data))
(println "setelah reduksi kedua:" @panggilan)sebelum: 0
reduksi pertama: 30
setelah reduksi pertama: 5
reduksi kedua: 30
setelah reduksi kedua: 5Reduksi pertama memicu realisasi seluruh elemen, terlihat dari penghitung yang melompat ke 5. Reduksi kedua menghasilkan nilai yang sama tanpa memanggil transformasi lagi karena elemen hasilnya masih tertaut pada var data. Pada skala sejuta elemen, seluruh sel seq yang telah dihitung tertahan di memori sampai rujukannya dilepas.
Pola yang lebih hemat memori adalah memproses data sampai selesai dalam satu langkah perangkuman, misalnya langsung dengan reduce atau into, tanpa mengikat seq antara dengan def. Laziness tetap dipertahankan di tengah alur, sedangkan hasil akhirnya berupa satu nilai atau koleksi konkret yang ukurannya memang diperlukan. Sudut pandang pengumpul sampah melengkapi gambaran ini. Selama var masih menunjuk head, tidak ada satu pun sel seq yang dapat dibebaskan karena seluruh rantai terhubung dari rujukan itu. Melepas rujukan, misalnya dengan tidak mengikat hasil sama sekali, memungkinkan seluruh rantai dikumpulkan begitu konsumsinya selesai.
Chunking: realisasi 32 elemen sekaligus#
Sebagian sumber seq, termasuk vector, string, dan range, direalisasikan per kelompok 32 elemen yang disebut chunk. Chunking membuat pembacaan satu elemen dari sumber tersebut otomatis merealisasikan seluruh kelompoknya. Demo berikut hanya meminta lima elemen.
(println (take 5 (map println (range 100))))(0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
nil nil nil nil nil)Yang diminta hanya lima elemen, tetapi println di dalam map berjalan 32 kali, yaitu untuk angka 0 sampai 31. Baris pertama diawali kurung karena println di luar mulai mencetak hasil take, dan pada saat elemen pertamanya diminta, realisasi chunk 32 elemen terjadi sekaligus. Baris terakhir berisi lima nil, yaitu nilai kembalian println di dalam map yang menjadi isi hasil take. Tambahan 27 baris keluaran itulah biaya chunking yang tidak terlihat pada kodenya.
Perbandingan dengan sumber tanpa chunking mempertegas penyebabnya.
(println (take 5 (map println (iterate inc 0))))(0
1
nil 2
nil 3
nil 4
nil nil)iterate merealisasikan elemen satu per satu, sehingga hanya lima angka 0 sampai 4 yang tercetak, persis jumlah yang diminta. Angka tercetak berselang-seling dengan nil karena setiap elemen direalisasikan pada saat pencetakan hasil take memintanya. Perbedaan keduanya tidak berasal dari take maupun map, melainkan dari sumber seq-nya.
Chunking dipilih demi kinerja, yaitu memangkas biaya per elemen dengan memproses 32 elemen dalam satu langkah pustaka. Manfaat itu berlaku untuk transformasi murni yang efeknya tidak teramati, tetapi berubah menjadi kejutan pada efek samping yang teramati.
Efek samping tertunda#
Karena evaluasi ditunda, efek samping di dalam map tidak berjalan ketika map dipanggil, melainkan ketika hasilnya direalisasikan. Kode berikut mencatat elemen yang sudah diproses ke dalam sebuah atom.
(def efek (atom []))
(def s (map (fn [x] (swap! efek conj x) x) (iterate inc 0)))
(println "efek sebelum realisasi:" (pr-str @efek))
(println "elemen kedua:" (second s))
(println "efek setelah second:" (pr-str @efek))efek sebelum realisasi: []
elemen kedua: 1
efek setelah second: [0 1]Sebelum ada pembacaan, catatan efek masih kosong. Pembacaan elemen kedua melalui second memicu realisasi elemen 0 dan 1, dan hanya keduanya yang tercatat karena sumber iterate tidak berchunking. Urutan pelaksanaan efek samping dengan demikian mengikuti urutan realisasi, bukan urutan penulisan kode. Pada sumber berchunking, pembacaan yang sama akan mencatat 32 elemen sekaligus.
Urutan realisasi ini juga menjelaskan mengapa catatan pada pipeline lazy tampak muncul belakangan, yaitu pencatatan terjadi ketika konsumen membaca data, bukan ketika pipeline disusun.
Seq tak terbatas dan fungsi rakus#
Fungsi yang perlu menelusuri seluruh elemen tidak akan pernah selesai atas seq tak terbatas. Kelompok ini mencakup count, last, doall, sort, into, dan apply, karena semuanya memerlukan seluruh elemen. Ekspresi berikut adalah contohnya.
; tidak-dijalankan
; count menuntut seluruh elemen seq, sedangkan (range) tak terbatas, sehingga
; ekspresi berikut tidak pernah selesai dan proses berputar tanpa akhir.
(println (count (range)))Ekspresi tersebut tidak pernah mengembalikan nilai karena count menuntut elemen terakhir yang tidak pernah ada, dan bloknya sengaja tidak dieksekusi. Gejala pada program nyata berupa proses yang tampak menggantung tanpa pesan error. Fungsi yang aman atas seq tak terbatas hanyalah yang meminta sebagian elemen, seperti take, take-while, drop, first, dan nth dengan indeks terbatas. Pembatasan dengan take pada ujung alur menjadi pengaman utama, yaitu sumber tak terbatas boleh mengalir selama konsumennya hanya meminta sebagian elemen.
Ringkasan#
- Menahan head melalui
defatas seq besar menyimpan seluruh elemen hasil realisasi di memori selama var tersebut masih menunjuk head. - Sumber berchunking direalisasikan per 32 elemen, sehingga efek samping dan biaya perhitungan dapat melampaui jumlah elemen yang diminta.
- Efek samping lazy berjalan saat realisasi terjadi, bukan saat
mapdipanggil. count,last,sort,into, dan fungsi rakus serupa tidak akan selesai atas seq tak terbatas.
Referensi#
- Sequences — Clojure — dokumentasi resmi laziness, termasuk peringatan tentang menahan head.
- clojure.core/doall — ClojureDocs — dokumentasi doall sebagai pemaksa realisasi yang menahan hasil.