For-comprehension

For-comprehension yang mudah dibaca beserta padanan flatMap dan map bersarang hasil desugaring.

Daftar isi

For-comprehension adalah gula sintaksis (syntactic sugar), yaitu bentuk penulisan yang nyaman dan secara mekanis diganti compiler dengan padanan fungsionalnya. Pada koleksi, padanannya berupa flatMap, map, dan withFilter. Topik ini menunjukkan bahwa kedua bentuk menghasilkan koleksi yang identik, merinci aturan desugaring yang diterapkan compiler, lalu menutupnya dengan generator berganda dan guard (guard) untuk menyaring nilai di tengah proses. Setelah membacanya, bentuk for dapat ditulis dengan keyakinan bahwa compiler menerjemahkannya menjadi padanan yang dapat dibaca kembali.

Dari flatMap bersarang ke for#

Menyusun pasangan dari dua koleksi secara fungsional berarti menyusun flatMap di dalam flatMap. Setiap generator pada for menjadi satu lapis bersarang, dan hanya generator terakhir yang diakhiri dengan map. Baris h <- hari disebut generator, yaitu ikatan yang menarik satu elemen dari koleksi di sebelah kanannya pada setiap langkah. Generator yang lebih dalam berjalan paling cepat, sehingga untuk setiap hari, seluruh jam dijalankan terlebih dahulu sebelum proses berpindah ke hari berikutnya. Program berikut menyusun jadwal sesi yang sama dalam dua bentuk, lalu membandingkan hasilnya.

scala kombinasi.scala
// Bentuk for dan padanan flatMap-map-nya menghasilkan koleksi yang sama
@main def kombinasi(): Unit =
  val hari: List[String] = List("Senin", "Selasa")
  val jam: List[Int] = List(9, 13)

  val gayaFlat: List[String] =
    hari.flatMap(h => jam.map(j => s"$h pukul $j"))

  val gayaFor: List[String] =
    for
      h <- hari
      j <- jam
    yield s"$h pukul $j"

  println(gayaFlat)
  println(gayaFor)
  println(gayaFlat == gayaFor)
bash
# menjalankan program kombinasi.scala
scala-cli run kombinasi.scala
text Keluaran
List(Senin pukul 9, Senin pukul 13, Selasa pukul 9, Selasa pukul 13)
List(Senin pukul 9, Senin pukul 13, Selasa pukul 9, Selasa pukul 13)
true

Nilai true pada baris ketiga berasal dari perbandingan == yang memeriksa kesetaraan struktural List, sehingga kesamaan kedua bentuk diperiksa langsung oleh program dan bukan sekadar klaim. Kedua baris pertama menampilkan koleksi yang sama persis, dengan empat pasangan sesi yang tersusun menurut urutan generatornya. Kata yield menyatakan ekspresi yang disusun untuk setiap kombinasi yang lolos, sehingga for-comprehension selalu menghasilkan koleksi dan bukan aksi. Tipe hasilnya mengikuti koleksi generator pertama, yaitu List pada contoh ini, dan setiap elemennya bertipe sama karena interpolasi string selalu menghasilkan String. Bentuk for menuliskan generator secara vertikal tanpa kurung penutup yang bertumpuk, dan itulah alasan utamanya lebih mudah dibaca ketika lapisannya bertambah.

Aturan desugaring#

Compiler mengubah for menurut tiga aturan yang tetap. Satu generator dengan yield menjadi map, dua generator atau lebih menjadi flatMap bersarang dengan map hanya pada generator terakhir, dan guard berupa if menjadi withFilter. Bentuk for yang isinya berupa aksi tanpa yield diterjemahkan menjadi foreach menurut aturan desugaring, sedangkan bentuk dengan yield itulah yang digunakan sepanjang topik ini.

Guard menggunakan withFilter dan bukan filter karena withFilter dirancang khusus untuk for-comprehension. Pada List, withFilter mengembalikan pandangan penyaring yang belum membangun koleksi perantara, sehingga guard tidak menambah alokasi baru di tengah desugaring.

scala Contoh: satu generator dengan yield menjadi map
for h <- hari yield singkat(h)
// menjadi
hari.map(h => singkat(h))
scala Contoh: dua generator menjadi flatMap bersarang
for
  h <- hari
  j <- jam
yield s"$h pukul $j"
// menjadi
hari.flatMap(h => jam.map(j => s"$h pukul $j"))
scala Contoh: guard menjadi withFilter
for
  n <- nilai
  if n >= 70
yield n
// menjadi
nilai.withFilter(n => n >= 70).map(n => n)

Demo berikut menampilkan ketiga bentuk tersebut berdampingan. Pilih salah satu bentuk for, kemudian bandingkan panel kanan yang memuat hasil desugaringnya.

Generator berganda dan guard#

Guard disisipkan di antara generator untuk menyaring nilai yang boleh melanjutkan, sedangkan generator berikutnya boleh bergantung pada ikatan sebelumnya. Desugaringnya menempatkan withFilter pada generator yang dijaga tanpa mengubah struktur bersarangnya. Guard boleh lebih dari satu dan boleh berada di antara generator mana pun, dan setiap guard menjadi withFilter tersendiri pada generator yang melingkupinya. Guard yang menumpuk pada satu generator biasanya lebih jelas jika digabung dengan && di dalam satu kondisi. Program berikut membandingkan bentuk for yang dijaga guard dengan padanan withFilter-nya.

scala guard.scala
// Guard pada for disugaring menjadi withFilter
@main def jaga(): Unit =
  val nilai: List[Int] = List(78, 45, 90, 62, 55, 84)

  val gayaFor: List[Int] =
    for
      n <- nilai
      if n >= 70
    yield n

  val gayaWithFilter: List[Int] =
    nilai.withFilter(n => n >= 70).map(n => n)

  println(gayaFor)
  println(gayaWithFilter)
  println(gayaFor == gayaWithFilter)
bash
# menjalankan program guard.scala
scala-cli run guard.scala
text Keluaran
List(78, 90, 84)
List(78, 90, 84)
true

Guard if n >= 70 menyaring List sebelum yield n dijalankan, dan padanan withFilter-nya menghasilkan keluaran yang sama. Perhatikan bahwa padanannya diakhiri dengan map(n => n) yang tampak tidak melakukan apa pun, karena aturan desugaring menambahkan map untuk setiap yield tanpa menilai isinya.

Generator berikutnya juga boleh menggunakan ikatan sebelumnya, sehingga pasangan yang dihasilkan membentuk pola segitiga.

scala Contoh: generator yang bergantung pada ikatan sebelumnya
for
  h <- 1 to 3
  j <- h to 3
yield (h, j)
text Keluaran
Vector((1,1), (1,2), (1,3), (2,2), (2,3), (3,3))

Ikatan 1 to 3 bertipe Range, dan for-comprehension di atasnya menghasilkan Vector yang isinya enam pasang berurutan. Nilai j hanya menelusuri angka yang tidak kurang dari h saat itu, karena generator kedua menunggu h diketahui. Pola ketergantungan semacam ini tidak dapat ditulis sebagai dua map terpisah, sehingga flatMap bersarang atau for-comprehension menjadi satu-satunya cara yang wajar.

Kapan bentuk for lebih dipilih#

Kedua bentuk setara secara hasil, sehingga pilihannya adalah soal keterbacaan. Pipeline satu atau dua tahap tetap wajar ditulis sebagai map dan filter langsung, karena rantainya pendek dan tanda titiknya mudah diikuti. Begitu generator bertambah atau saling bergantung, keunggulan bentuk for segera tampak, karena bersarangnya terlihat sebagai blok vertikal dan setiap ikatan diberi nama sendiri.

Pertimbangan yang sama berlaku saat guard ikut menumpuk. Rantai withFilter yang berulang pada satu baris sulit dibaca, sedangkan baris if yang berdiri sendiri di dalam for langsung menunjukkan syaratnya. Konsistensi satu gaya di dalam satu proyek lebih penting daripada preferensi pribadi, dan konvensi yang umum menggunakan for untuk kombinasi dua generator atau lebih.

Ringkasan#

  • For-comprehension pada koleksi adalah gula sintaksis yang diterjemahkan compiler menjadi flatMap, map, dan withFilter melalui desugaring, sehingga hasilnya identik dengan padanan fungsionalnya.
  • Aturan desugaringnya tetap: satu generator menjadi map, generator berganda menjadi flatMap bersarang dengan map pada generator terakhir, dan guard menjadi withFilter.
  • Guard menyaring nilai di tengah for tanpa mengubah struktur bersarang, dan generator berikutnya boleh bergantung pada ikatan sebelumnya.
  • Keterbacaan adalah alasan utama memilih bentuk for ketika lapisan bersarang bertambah banyak.

Referensi#