Comprehension dengan for

for dengan generator dan filter untuk membangun koleksi.

Daftar isi

Topik ini membahas comprehension, yakni sintaks for untuk membangun koleksi baru dari satu atau beberapa sumber data. Pembahasan mencakup generator sebagai sumber elemen, generator ganda untuk kombinasi, filter untuk penyaringan, opsi into: untuk mengubah wadah hasil, dan opsi reduce: untuk akumulasi. Setelah topik ini dibaca, for dapat dipilih sebagai alat pembangun koleksi dan dibedakan dari perulangan yang sekadar menjalankan efek samping.

For sebagai pembangun koleksi#

for di Elixir bukan perulangan biasa melainkan comprehension, yaitu ekspresi yang selalu menghasilkan koleksi baru. Klausa generator (generator) berbentuk pola <- enumerable dan mengambil setiap elemen sumbernya, sedangkan blok do menyatakan nilai yang dikumpulkan untuk setiap elemen. Kata kunci <- dibaca sebagai pengambilan elemen, sehingga generator n <- [1, 2, 3, 4] menarik setiap bilangan dari listnya satu per satu. Hasil bawaannya berupa list, dan ekspresi for dapat diikat ke variabel seperti nilai kembalian fungsi lain.

elixir for_dasar.exs
# for sebagai ekspresi pembangun list
kuadrat = for n <- [1, 2, 3, 4], do: n * n
IO.inspect(kuadrat, label: "kuadrat")

kombinasi = for x <- [1, 2], y <- ["a", "b"], do: {x, y}
IO.inspect(kombinasi, label: "kombinasi")
text Keluaran
kuadrat: [1, 4, 9, 16]
kombinasi: [{1, "a"}, {1, "b"}, {2, "a"}, {2, "b"}]

Generator kedua pada contoh diulang untuk setiap elemen generator pertama, sehingga hasilnya adalah kombinasi silang yang tersusun menurut urutan generatornya. Urutan ini meniru perulangan bersarang: generator paling kiri berubah paling lambat. Menukar posisi kedua generator pada contoh menghasilkan pasangan yang urutannya ikut bertukar. Sumber generator tidak harus berupa list, karena rentang dan map yang merupakan enumerable juga dapat digunakan. Bentuk do: pada contoh digunakan untuk isi satu baris, sedangkan bentuk blok do dan end digunakan pada opsi into: dan reduce: di bagian berikutnya. Variabel yang diikat di dalam comprehension memiliki lingkup sendiri, sehingga tidak bocor ke luar ekspresinya.

Generator ganda dan filter#

Syarat ditulis sebagai klausa filter (filter) setelah generator, dan hanya elemen yang membuat syarat bernilai true yang lolos. Klausa ini dipisahkan koma dari generatornya. Syaratnya boleh berupa ekspresi apa pun yang menghasilkan boolean, misalnya rem/2 untuk mengambil kelipatan tertentu dari sebuah rentang.

elixir Contoh: filter kelipatan tiga
kelipatan = for n <- 1..12, rem(n, 3) == 0, do: n
IO.inspect(kelipatan, label: "kelipatan tiga")
text Keluaran
kelipatan tiga: [3, 6, 9, 12]

Generator juga menerima pola, dan pola yang tidak cocok membuat elemen sumber dibuang tanpa pesan. Pencocokan yang gagal tidak memunculkan error karena kegagalannya dimaknai sebagai penolakan elemen. Dengan demikian, generator berpola sekaligus bertindak sebagai filter, yang berguna ketika sumber data berisi bentuk campuran, misalnya keyword list hasil operasi yang berhasil dan yang gagal.

elixir Contoh: pola {:ok, v} menyaring tuple gagal
hasil = [ok: 78, error: :kosong, ok: 92, error: :duplikat]

nilai_sah = for {:ok, v} <- hasil, do: v

IO.puts("Nilai sah: " <> Enum.join(Enum.map(nilai_sah, &Integer.to_string/1), ", "))
text Keluaran
Nilai sah: 78, 92

Filter dan pola dapat dipadukan dalam satu comprehension karena keduanya hanyalah klausa berurutan. Elemen harus lolos seluruh klausa sebelum blok do dievaluasi, sehingga penyaringan bertingkat tetap terbaca sebagai satu baris deklarasi. Filter dapat ditempatkan di antara dua generator, dan filter tersebut dapat membaca nilai generator di depannya, sehingga kombinasi yang tidak mungkin lolos tidak ikut dihitung.

Membangun map dan MapSet dengan into#

Tanpa opsi tambahan, for selalu menghasilkan list. Opsi into: mengganti wadah hasilnya, misalnya into: %{} untuk menyusun map dari pasangan kunci-nilai atau into: MapSet.new() untuk mengumpulkan elemen yang unik dalam himpunan. Wadah sasaran harus mengimplementasikan protokol Collectable, yaitu protokol untuk memasukkan elemen satu per satu.

elixir into_map_dan_mapset.exs
# Opsi into: wadah hasil berupa map dan MapSet
kamus =
  for {hari, urut} <- Enum.with_index(["Senin", "Selasa", "Rabu"]), into: %{} do
    {urut, hari}
  end

IO.inspect(kamus, label: "kamus")

unik = for n <- [3, 1, 3, 2, 1], into: MapSet.new(), do: n
IO.inspect(unik, label: "unik")
text Keluaran
kamus: %{0 => "Senin", 1 => "Selasa", 2 => "Rabu"}
unik: MapSet.new([1, 2, 3])

Contoh kamus menggunakan Enum.with_index/1 yang melampirkan nomor urut pada setiap elemen, sehingga tiap iterasi menghasilkan pasangan yang siap dimasukkan ke map. Penomoran dimulai dari nol, sehingga kunci kamus berupa 0, 1, dan 2. MapSet sendiri otomatis membuang duplikat, sehingga pola into: MapSet.new() menjadi cara ringkas mendapatkan elemen unik tanpa fungsi tambahan. Apabila dua iterasi menghasilkan kunci map yang sama, pasangan terakhir yang menang sebagaimana pada map biasa. Opsi into: juga menerima binary kosong, sehingga into: "" menyambung seluruh hasil menjadi satu string.

Akumulasi dengan reduce#

Ketika hasil yang diinginkan bukan koleksi melainkan satu nilai akumulasi, opsi reduce: menggantikan wadah hasil. Sintaksnya berupa blok do dengan variabel akumulator, dan setiap iterasi mengembalikan akumulator yang diperbarui karena nilai bersifat imutabel. Struktur akumulator boleh serumit apa pun, misalnya map yang menampung rekapitulasi per kelompok. Mekanismenya menyerupai Enum.reduce/3, tetapi digabungkan langsung dengan generator dan filter comprehension sehingga penyaringan dan akumulasi terjadi dalam satu ekspresi.

elixir Contoh: menjumlahkan bilangan genap dengan reduce
total_genap =
  for n <- 1..10, reduce: 0 do
    acc -> if rem(n, 2) == 0, do: acc + n, else: acc
  end

IO.inspect(total_genap, label: "total genap")
text Keluaran
total genap: 30

Perhatikan bahwa contoh di atas menyeleksi bilangan genap di dalam blok do, bukan pada klausa filter. Seleksi yang sama dapat dipindahkan menjadi for n <- 1..10, rem(n, 2) == 0, reduce: 0, dan hasilnya identik. Blok do pada reduce hanya menerima satu variabel, yaitu akumulator, sedangkan elemennya tersedia dari generator di depannya. Kombinasi dengan klausa filter membuat akumulasi hanya berjalan pada elemen yang lolos. Nilai 30 pada keluaran adalah jumlah seluruh bilangan genap dari 1 sampai 10.

Kesalahan umum#

Kesalahan pertama adalah tidak menyadari sifat pola pada generator. Elemen yang tidak cocok dengan pola dibuang diam-diam, sehingga data yang hilang tidak menyisakan jejak. Apabila elemen yang gagal tetap harus terhitung, lakukan pencocokan di dalam blok do terhadap kedua bentuk tuple, atau susun dua comprehension terpisah yang masing-masing menangani satu bentuk.

Kesalahan kedua adalah menyusun map tanpa opsi into:. Comprehension tanpa into: menghasilkan list berisi tuple, dan struktur itu belum menjadi map walaupun elemennya berbentuk pasangan. Struktur tersebut masih dapat dikonversi dengan Enum.into/2, tetapi menambahkan into: %{} langsung pada comprehension lebih ringkas dan menghasilkan map yang kuncinya dapat diakses.

Kesalahan ketiga adalah memperlakukan for sebagai perulangan serba guna. for ditujukan untuk membangun nilai, sedangkan deretan efek samping tanpa hasil, misalnya mencetak setiap baris laporan, dinyatakan dengan Enum.each/2 agar maksudnya jelas. Fungsi Enum.each/2 mengembalikan atom :ok, dan bentuk kembaliannya menegaskan bahwa tidak ada nilai yang dikumpulkan. Memilih alat yang tepat membuat maksud kode terbaca dari bentuknya.

Ringkasan#

  • for adalah comprehension yang selalu menghasilkan koleksi baru, dengan generator pola <- enumerable sebagai sumber elemennya.
  • Generator ganda menghasilkan kombinasi silang, sedangkan klausa filter dan pola pada generator menyaring elemen, termasuk diam-diam membuang elemen yang tidak cocok.
  • Opsi into: mengganti wadah hasil menjadi map, MapSet, atau wadah Collectable lain.
  • Opsi reduce: mengubah comprehension menjadi akumulasi satu nilai tanpa keluar dari sintaks for.

Referensi#