Transformasi

map, filter, dan flatMap beserta biaya alokasi yang menyertai setiap tahap transformasi.

Daftar isi

Transformasi adalah operasi yang menerima sebuah koleksi dan menghasilkan koleksi baru, tanpa mengubah koleksi asalnya. Tiga metode inti pada keluarga ini adalah map untuk mengubah setiap elemen, filter untuk menyaring dengan predikat, dan flatMap untuk mengubah setiap elemen menjadi koleksi lalu meratakannya. Topik ini membahas ketiganya pada contoh nilai ujian dan jadwal piket, beserta biaya alokasi yang menyertai setiap tahap dan cara menunda evaluasinya dengan view.

map dan filter sebagai tahap pipeline#

map menerima fungsi satu argumen dan menerapkannya pada setiap elemen. Hasilnya adalah koleksi baru dengan jumlah elemen yang sama, sedangkan tipe elemennya boleh berbeda dari asalnya, misalnya Int menjadi String. filter menerima predikat, yaitu fungsi yang menghasilkan Boolean, dan hanya menyimpan elemen yang membuat predikatnya bernilai true, sehingga jumlah elemennya dapat menyusut sementara tipenya tetap.

Penulisan fungsi pada contoh menggunakan placeholder _, yaitu singkatan untuk parameter yang hanya digunakan satu kali, sehingga map(_ - 5) sama artinya dengan map(n => n - 5). Apabila logikanya membutuhkan lebih dari satu langkah, fungsi biasa dengan parameter bernama ditulis utuh. Konsistensi bentuk ini menjaga pipeline tetap terbaca sebagai satu kalimat dari kiri ke kanan.

Program berikut mengoreksi sekumpulan nilai ujian dengan map, lalu menyaring nilai yang lulus dengan filter. Kedua tahap dirangkai menjadi pipeline, yaitu rantai operasi yang keluarannya menjadi masukan bagi tahap berikutnya.

scala transformasi.scala
// Pipeline map dan filter pada nilai ujian
@main def transformasi(): Unit =
  val nilai: List[Int] = List(78, 45, 90, 62, 55, 84)

  val terkoreksi: List[Int] = nilai.map(_ - 5)
  val lulus: List[Int] = terkoreksi.filter(n => n >= 70)

  println(terkoreksi)
  println(lulus)
bash
# menjalankan program transformasi.scala
scala-cli run transformasi.scala
text Keluaran
List(73, 40, 85, 57, 50, 79)
List(73, 85, 79)

Keluaran memperlihatkan dua koleksi baru. Baris pertama berisi hasil koreksi untuk seluruh nilai, sedangkan baris kedua hanya memuat nilai yang mencapai batas 70 setelah dikoreksi. Koleksi asal nilai tidak tersentuh di sepanjang proses, sehingga tahap yang sama dapat diulang dengan batas kelulusan berbeda tanpa menyiapkan ulang datanya.

flatMap: transformasi yang meratakan koleksi#

Apabila fungsi transformasinya sendiri menghasilkan koleksi, map akan menghasilkan koleksi di dalam koleksi. flatMap menerapkan fungsi yang sama lalu meratakan hasilnya menjadi satu tingkat, sehingga jumlah elemen akhir dapat bertambah, berkurang, atau tetap. Bentuk inilah yang menjadi fondasi desugaring for-comprehension, yaitu penurunan notasi for menjadi rangkaian flatMap dan map.

Contoh berikut menyusun jadwal piket. Setiap hari memiliki dua sesi, sehingga fungsi transformasinya menghasilkan List berisi dua string untuk setiap hari dan flatMap meratakannya menjadi satu jadwal datar.

scala jadwal-piket.scala
// flatMap menjalankan transformasi yang setiap hasilnya berupa koleksi, lalu meratakannya
@main def jadwalPiket(): Unit =
  val hari: List[String] = List("Senin", "Selasa", "Rabu")

  val sesi: List[String] = hari.flatMap(h => List(s"$h pagi", s"$h siang"))

  println(sesi)
bash
# menjalankan program jadwal-piket.scala
scala-cli run jadwal-piket.scala
text Keluaran
List(Senin pagi, Senin siang, Selasa pagi, Selasa siang, Rabu pagi, Rabu siang)

Tiga hari dengan dua sesi menghasilkan enam entri, dan semuanya berada pada satu List, bukan tiga List di dalam sebuah List. Apabila fungsi transformasinya menghasilkan Nil untuk hari tertentu, hari itu tidak menyumbang elemen apa pun. Karena itu, flatMap juga berguna untuk mengubah dan menyaring sekaligus dalam satu langkah.

Biaya alokasi setiap tahap#

Pada koleksi ketat (strict), setiap tahap transformasi menelusuri seluruh elemen koleksinya dan mengalokasikan satu koleksi baru. Pipeline dua tahap pada contoh nilai ujian dengan demikian melakukan dua penelusuran O(n)O(n) dan membuat dua koleksi sementara, meskipun program hanya menggunakan hasil tahap terakhir. Diagram berikut menggambarkan alur alokasi tersebut.

List6 nilai ujianmap(_ - 5)List baru6 nilai terkoreksifilter(_ >= 70)List baru3 nilai lulusSetiap tahap menelusuri seluruh elemen dan mengalokasikan koleksi baru
Alokasi koleksi baru pada setiap tahap pipeline map dan filter

Pada contoh enam nilai ujian, dua tahap berarti dua belas kunjungan elemen dan dua alokasi koleksi. Polanya tumbuh linier, yaitu kk tahap pada nn elemen menghabiskan k×nk \times n kunjungan dan kk alokasi, sedangkan ukuran tiap koleksi tetap proporsional dengan data yang lolos. Angka sebesar ini tidak terasa pada data sehari-hari, tetapi kelipatannya menumpuk ketika data dan jumlah tahapnya membesar.

Biaya ini biasanya dapat diterima karena tetap linier terhadap jumlah data, dan pustaka immutable memang dirancang untuk menyalin strukturnya secara murah. Perhatian baru muncul pada data besar atau pipeline yang panjang. Urutan tahap juga memengaruhi jumlah pekerjaan, karena memfilter terlebih dahulu menyusutkan data sebelum tahap map berjalan. Kombinasi filter terlebih dahulu lalu map umumnya lebih murah daripada sebaliknya apabila filtrenya menyusutkan banyak elemen.

View: pipeline yang menjadi satu penelusuran#

view mengubah koleksi menjadi View, yaitu barisan elemen yang transformasinya ditunda. Pada View, map dan filter tidak langsung memproses elemen, melainkan hanya mencatat rencananya. Elemen benar-benar diproses ketika koleksi akhir diminta, misalnya dengan toList, sehingga seluruh pipeline menjadi satu penelusuran dan hanya satu koleksi akhir yang dialokasikan.

Program berikut menuliskan ulang pipeline nilai ujian dengan view. Isinya identik dengan pipeline ketat pada bagian sebelumnya, sehingga perbedaannya murni terletak pada jumlah penelusuran dan alokasi.

scala view.scala
// view menunda setiap tahap sampai koleksi akhir diminta
@main def pandangan(): Unit =
  val nilai: List[Int] = List(78, 45, 90, 62, 55, 84)

  val hasil: List[Int] = nilai.view.map(_ - 5).filter(n => n >= 70).toList

  println(hasil)
bash
# menjalankan program view.scala
scala-cli run view.scala
text Keluaran
List(73, 85, 79)

Pada program kecil, perbedaan ini tidak terasa, tetapi pola penulisannya penting untuk dibiasakan. Penggunaan view menjadi relevan ketika data besar melewati beberapa tahap, karena penelusuran dan alokasi sementara berkurang dari satu per tahap menjadi satu untuk seluruh pipeline. Aturan praktisnya, view ditambahkan ketika pipeline mulai panjang dan datanya besar, bukan pada setiap pipeline dua tahap, karena keterbacaan pipeline ketat yang pendek lebih berharga daripada penghematannya.

Ringkasan#

  • map menghasilkan koleksi baru dengan jumlah elemen yang sama, filter menyusutkan isinya menurut predikat, dan flatMap meratakan hasil transformasi yang berupa koleksi.
  • Koleksi asal tidak pernah berubah, karena setiap transformasi mengalokasikan koleksi baru pada pustaka immutable.
  • Setiap tahap pipeline ketat menelusuri seluruh elemen dan mengalokasikan satu koleksi, sehingga pipeline kk tahap melakukan kk penelusuran.
  • view menunda setiap tahap sampai koleksi akhir diminta, sehingga seluruh pipeline menjadi satu penelusuran dengan satu alokasi di ujungnya.

Referensi#