Comprehension dan Generator

Membentuk array dengan comprehension dan mengiterasi secara malas menggunakan generator.

Daftar isi

Comprehension membentuk array baru dari sebuah objek yang dapat diiterasi dengan satu baris notasi, sedangkan generator mengiterasi nilai yang sama secara malas (lazy) tanpa menyimpannya. Topik ini membahas comprehension satu dimensi, penyaringan dengan kondisi, comprehension multidimensi, generator sebagai iterator malas, serta perbandingan alokasi memori keduanya. Setelah topik ini dipahami, cara membentuk kumpulan data baru dapat dipilih sesuai kebutuhan memori dan tujuan pengolahannya.

Comprehension satu dimensi#

Comprehension, yakni pembentukan array dari perulangan dalam satu notasi, ditulis dengan tanda kurung siku yang memuat satu ekspresi beserta perulangannya. Bentuk umumnya [ekspresi for x in koleksi], dan ekspresi itu dievaluasi untuk setiap elemen koleksi. Hasilnya selalu array baru yang berisi seluruh nilai, sedangkan variabel perulangannya hanya hidup di dalam tanda kurung tersebut.

julia Contoh: comprehension kuadrat
kuadrat = [x^2 for x in 1:5]
println(kuadrat)
println(typeof(kuadrat))
text Keluaran
[1, 4, 9, 16, 25]
Vector{Int64}

Ekspresinya boleh memanggil fungsi apa pun. Contoh berikut menghitung panjang setiap kata pada sebuah array.

julia Contoh: comprehension dengan pemanggilan fungsi
panjang_kata = [length(k) for k in ["ali", "budiono", "citra"]]
println(panjang_kata)
text Keluaran
[3, 7, 5]

Ekspresinya juga boleh berupa ekspresi bersyarat. Contoh berikut menggabungkan operator ternary dari topik percabangan dengan comprehension untuk melabeli setiap bilangan.

julia Contoh: ekspresi bersyarat di dalam comprehension
status = [x % 2 == 0 ? "genap" : "ganjil" for x in 1:5]
println(status)
text Keluaran
["ganjil", "genap", "ganjil", "genap", "ganjil"]

Penyaringan dengan kondisi#

Klausa if di dalam comprehension menyaring elemen yang ikut dihasilkan. Elemen tetap diiterasi seluruhnya, tetapi hanya elemen yang memenuhi kondisi yang dimasukkan ke hasil. Kondisi yang lebih spesifik dapat digabung dengan operator &&, dan apabila syaratnya rumit, membentuknya sebagai fungsi terpisah menjaga comprehension tetap terbaca.

julia Contoh: comprehension dengan penyaringan
genap = [x for x in 1:10 if x % 2 == 0]
println(genap)

habis_dibagi_tiga = [x for x in 1:10 if x % 3 == 0 && x % 2 == 1]
println(habis_dibagi_tiga)
text Keluaran
[2, 4, 6, 8, 10]
[3, 9]

Klausa penyaringan tidak mengubah jenis hasilnya. Hasil tetap berupa array yang hanya menyimpan elemen lolos syarat, sehingga banyaknya elemen hasil tidak diketahui sebelum perulangan berjalan. Untuk data yang sangat panjang, penyaringan seperti ini menggantikan perulangan for beserta pernyataan if yang panjangnya dua kali lipat.

Comprehension multidimensi#

Comprehension dengan dua perulangan yang dipisah koma membentuk array multidimensi. Bentuk [ekspresi for r in 1:3, c in 1:3] menghasilkan matriks 3×3 yang setiap elemennya dihitung dari satu kombinasi nilai r dan c. Rentang kedua perulangan tidak harus sama panjang, misalnya 1:3 dan 1:4 menghasilkan matriks 3×4. Koma menetapkan dimensi keluaran, sehingga hasilnya matriks, berbeda dengan bentuk bersarang yang menghasilkan array berisi array.

julia Contoh: comprehension multidimensi
matriks = [r * c for r in 1:3, c in 1:3]
println(matriks)
println(typeof(matriks))
text Keluaran
[1 2 3; 2 4 6; 3 6 9]
Matrix{Int64}

Apabila perulangan kedua diletakkan di dalam tanda kurung siku yang baru, setiap elemen hasilnya berupa array. Bentuk bersarang ini tepat apabila struktur data bertingkat memang diinginkan, misalnya barisan nilai per baris.

julia Contoh: comprehension bersarang
bersarang = [[r * c for c in 1:3] for r in 1:3]
println(bersarang)
text Keluaran
[[1, 2, 3], [2, 4, 6], [3, 6, 9]]

Generator sebagai iterator malas#

Generator ditulis dengan tanda kurung biasa, misalnya (x^2 for x in 1:5), dan menghasilkan iterator alih-alih array, yaitu objek yang memberikan nilai satu per satu. Nilainya tidak dihitung pada saat generator dibentuk, melainkan saat diambil satu per satu oleh konsumennya, misalnya sum atau collect. Perilaku menunda perhitungan ini disebut evaluasi malas. Diagram berikut membandingkan keduanya.

comprehension: segera1:5segera1491625seluruh nilai langsung tersimpan di memorigenerator: malas1:5(x^2)1nilai dibuat saat diminta
Perbedaan comprehension yang langsung terisi dan generator yang menghasilkan nilai saat diminta
julia Contoh: generator dan konsumennya
g = (x^2 for x in 1:5)
println(length(g))
println(sum(g))
println(collect(g))
text Keluaran
5
55
[1, 4, 9, 16, 25]

Fungsi sum menghitung totalnya tanpa membentuk array perantara, sedangkan collect secara eksplisit mengumpulkan seluruh nilai menjadi array. Generator juga dapat langsung dipakai sebagai sumber perulangan for, sehingga kode yang mengonsumsinya tidak perlu tahu bahwa sumbernya malas.

julia Contoh: mengonsumsi generator dengan for
for nilai in (x^2 for x in 1:3)
    println(nilai)
end
text Keluaran
1
4
9

Untuk mengamati perbedaan perilaku comprehension dan generator secara langsung, jalankan demo berikut, tarik nilai pada panel generator satu per satu, kemudian bandingkan dengan panel comprehension.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Peraga comprehension dan generator</title>
<style>
  body { font-family: system-ui, sans-serif; margin: 0; padding: 14px; color: #1a1a1a; background: #ffffff; }
  .kolom { border: 1px solid #d6dde7; border-radius: 8px; padding: 10px 12px; margin-bottom: 10px; }
  .kolom h3 { margin: 0 0 8px; font-size: 14px; }
  .kolom p { margin: 8px 0 0; font-size: 12.5px; color: #555555; }
  button { padding: 6px 12px; border: 0; border-radius: 6px; background: #35567e; color: #ffffff; font-size: 13px; cursor: pointer; }
  button:hover { background: #284463; }
  button.malas { background: #6d4f96; }
  button.malas:hover { background: #583f7b; }
  button.kosong { background: #888888; }
  .chip { display: inline-block; min-width: 30px; text-align: center; padding: 4px 6px; margin: 3px 3px 0 0; border-radius: 5px; font-family: ui-monospace, monospace; font-size: 13px; }
  .chip.segera { background: #e3ecf7; border: 1px solid #46648c; }
  .chip.malas { background: #efe7f7; border: 1px solid #7a5a9e; }
  .chip.masa-depan { background: #f4f4f4; border: 1px dashed #999999; color: #999999; }
  #status { font-size: 12.5px; color: #555555; margin-top: 6px; }
</style>
</head>
<body>
  <div class="kolom">
    <h3>Comprehension: segera</h3>
    <button id="bentuk" type="button">Bentuk array [x^2 for x in 1:5]</button>
    <div id="kotak-segera"></div>
    <p id="info-segera">Kelima nilai terbentuk sekaligus pada satu langkah.</p>
  </div>
  <div class="kolom">
    <h3>Generator: malas</h3>
    <button id="tarik" type="button">Tarik satu nilai (x^2 for x in 1:5)</button>
    <button id="ulang" type="button">Mulai ulang</button>
    <div id="kotak-malas"></div>
    <div id="status">Belum ada nilai yang diminta. Array tidak pernah dialokasikan.</div>
  </div>
  <script>
    var terkini = 0;
    var sumber = [1, 2, 3, 4, 5];
    function chip(teks, kelas) {
      var el = document.createElement('span');
      el.className = 'chip ' + kelas;
      el.textContent = teks;
      return el;
    }
    function gambarMasaDepan() {
      var kotak = document.getElementById('kotak-malas');
      kotak.innerHTML = '';
      for (var i = 0; i < terkini; i++) {
        kotak.appendChild(chip(String(sumber[i] * sumber[i]), 'malas'));
      }
      for (var j = terkini; j < 5; j++) {
        kotak.appendChild(chip('?', 'masa-depan'));
      }
      var status = document.getElementById('status');
      if (terkini === 0) {
        status.textContent = 'Belum ada nilai yang diminta. Array tidak pernah dialokasikan.';
      } else if (terkini < 5) {
        status.textContent = 'Nilai ke-' + terkini + ' dihasilkan saat diminta; sisanya belum dihitung.';
      } else {
        status.textContent = 'Kelima nilai telah ditarik satu per satu. Tidak ada array yang dialokasikan.';
      }
    }
    document.getElementById('bentuk').addEventListener('click', function () {
      var kotak = document.getElementById('kotak-segera');
      kotak.innerHTML = '';
      for (var i = 0; i < sumber.length; i++) {
        kotak.appendChild(chip(String(sumber[i] * sumber[i]), 'segera'));
      }
      document.getElementById('info-segera').textContent = 'Kelima nilai terbentuk sekaligus dan tersimpan dalam array.';
    });
    document.getElementById('tarik').addEventListener('click', function () {
      if (terkini < 5) { terkini += 1; }
      gambarMasaDepan();
    });
    document.getElementById('ulang').addEventListener('click', function () {
      terkini = 0;
      gambarMasaDepan();
    });
  </script>
</body>
</html>

Perbandingan alokasi memori#

Perbedaan paling nyata antara comprehension dan generator adalah alokasi memori. Comprehension selalu mengalokasikan array baru sebesar hasilnya, sedangkan generator hampir tidak mengalokasikan apa pun selama nilainya dikonsumsi aliran demi aliran. Makro @allocated mengukur banyaknya byte yang dialokasikan oleh sebuah ekspresi, dan pengukurannya dilakukan setelah pemanasan agar kode sudah terkompilasi.

julia alokasi.jl
# alokasi.jl
# Membandingkan alokasi memori antara comprehension dan generator.
# @allocated diukur setelah pemanasan agar hasilnya stabil.

n = 1000

membentuk_array(n) = [x^2 for x in 1:n]
menghitung_total(n) = sum(x^2 for x in 1:n)

# pemanasan: kedua fungsi telah dikompilasi sebelum pengukuran
membentuk_array(n)
menghitung_total(n)

print("alokasi comprehension: ")
println(@allocated membentuk_array(n))
print("alokasi generator:     ")
println(@allocated menghitung_total(n))
println("total kuadrat: ", menghitung_total(n))
text Keluaran
alokasi comprehension: 8072
alokasi generator:     0
total kuadrat: 333833500

Array berisi 1000 bilangan Int64 menempati 8000 byte data, dan sisanya merupakan struktur internal array, sedangkan generator mengalokasikan 0 byte karena total dihitung langsung dari perulangannya. Untuk data berukuran besar yang nilainya hanya diproses sekali, generator menghemat memori secara nyata. Perlu dicatat bahwa @allocated hanya mengukur alokasi, bukan waktu berjalan, dan angkanya dapat berubah antar versi Julia, sehingga nilainya bermakna sebagai perbandingan pada satu lingkungan yang sama.

Kesalahan umum#

Kesalahan berikut paling sering muncul saat topik ini dipraktikkan. Dua yang pertama dapat direproduksi dan ditangkap pada contoh berikut.

julia Contoh: dua kesalahan yang tertangkap
g = (x^2 for x in 1:5)
try
    println(g[1])
catch e
    println(typeof(e))
end

[x^2 for x in 1:5]
try
    println(x)
catch e
    println(typeof(e))
end
text Keluaran
MethodError
UndefVarError
  • Mengindeks generator seperti array. Generator tidak mendukung pengindeksan karena nilai berikutnya baru dihasilkan saat diminta, sehingga g[1] berhenti dengan MethodError. Gunakan collect(g) lebih dahulu apabila akses acak benar-benar diperlukan, dengan konsekuensi alokasi array.
  • Mengira variabel perulangan masih dapat diakses setelah comprehension. Variabel x pada [x^2 for x in 1:5] hanya hidup di dalam comprehension, sehingga pembacaan x sesudahnya berhenti dengan UndefVarError.
  • Memilih comprehension untuk data yang sangat besar padahal nilainya hanya dijumlahkan sekali. Comprehension mengalokasikan seluruh array lebih dahulu, sedangkan generator menghitung nilainya secara berurutan tanpa array perantara.

Ringkasan#

  • Comprehension [ekspresi for x in koleksi] membentuk array baru yang berisi seluruh nilai, dengan klausa if untuk penyaringan dan koma untuk menetapkan dimensi tambahan.
  • Generator (ekspresi for x in koleksi) menghasilkan iterator malas yang menghitung nilai saat diminta, sehingga sum bekerja tanpa array perantara.
  • @allocated memperlihatkan perbedaannya secara langsung, yaitu alokasi array penuh oleh comprehension dan nol byte oleh generator yang dikonsumsi aliran demi aliran.

Referensi#