Pipeline koleksi

Menyusun pipeline data dengan findAll, collect, groupBy, inject, dan sort.

Daftar isi

Metode ekstensi GDK mengubah koleksi menjadi saluran pemrosesan data yang disusun tahap demi tahap, yakni pipeline. Topik ini membahas lima metode intinya, yaitu findAll untuk menyaring, collect untuk mengubah, groupBy untuk mengelompokkan, inject untuk menumpuk nilai, serta sort untuk mengurutkan. Setelah membaca topik ini, sebuah daftar data mentah dapat diolah menjadi ringkasan yang siap ditampilkan hanya dengan beberapa baris closure yang berantai.

Menyaring dengan findAll#

Metode findAll mengembalikan elemen-elemen yang membuat closure-nya bernilai benar, dan list aslinya tidak berubah. Closure menerima satu parameter untuk list, sedangkan pada map closure menerima dua parameter, yaitu kunci dan nilai, dan hasilnya tetap berupa map yang hanya memuat entri terpilih. Urutan elemen pada hasil mengikuti urutan aslinya, dan bila tidak ada satu pun elemen yang cocok, hasilnya tetap berupa list atau map kosong, bukan null. Peran ini menjadikan findAll tahap penyaring yang lazim dipasang sebelum elemen terpilih diubah oleh tahap berikutnya.

groovy Contoh: menyaring skor dengan findAll
def skor = [90, 75, 88, 70]
def tinggi = skor.findAll { it >= 80 }
println tinggi
println skor
text Keluaran
[90, 88]
[90, 75, 88, 70]

Baris kedua keluaran menegaskan bahwa findAll menghasilkan list baru, karena skor tetap memuat keempat nilai aslinya setelah penyaringan. Pada map, bentuk dua parameter memisahkan kunci dari nilainya sehingga penyaringan dapat mendasarkan diri pada salah satunya. Keluaran [Ali:90, Citra:88] mempertahankan pasangan utuhnya, sehingga nama tetap melekat pada skornya tanpa pencocokan manual.

groovy Contoh: findAll pada map
def nilai = [Ali: 90, Budi: 75, Citra: 88, Dewi: 70]
def memuaskan = nilai.findAll { nama, skor -> skor >= 80 }
println memuaskan
text Keluaran
[Ali:90, Citra:88]

Mengubah dengan collect#

Metode collect menerapkan closure pada setiap elemen dan mengumpulkan hasilnya menjadi list baru. Bentuk ini adalah pengubah bentuk data, bukan penyaring, karena jumlah elemen keluarannya sama dengan masukannya. Pada map, closure satu parameter menerima objek Entry, sedangkan closure dua parameter menerima kunci dan nilai secara terpisah.

groovy Contoh: mengubah elemen dengan collect
def skor = [90, 75, 88, 70]
def tambahan = skor.collect { it + 5 }
println tambahan
def pasangan = [Ali: 90, Budi: 75].collect { kunci, nilai -> "$kunci = $nilai" }
println pasangan
text Keluaran
[95, 80, 93, 75]
[Ali = 90, Budi = 75]

Karena keluaran collect juga sebuah list, hasilnya dapat langsung diberikan kepada metode pipeline berikutnya. Pola inilah yang membuat tahapan pemrosesan tersambung tanpa variabel perantara apabila dibutuhkan. Untuk rantai yang panjang, penulisan bertahap dengan variabel perantara justru lebih mudah diperiksa, karena nilai setiap tahap dapat dicetak satu per satu ketika hasilnya menyimpang. Pilihan keduanya bukan soal benar salah, melainkan soal seberapa banyak titik pemeriksaan yang dibutuhkan.

Mengelompokkan dengan groupBy#

Metode groupBy membagi elemen ke dalam map berdasarkan nilai yang dikembalikan closure-nya, dan hasilnya adalah LinkedHashMap yang kuncinya berasal dari nilai kembalian closure tersebut. Setiap kunci memetakan ke list yang memuat seluruh elemen anggota kelompoknya, sehingga struktur hasilnya berlapis satu tingkat. Kunci baru terdaftar pada kemunculan pertamanya, sehingga urutan kunci mengikuti urutan elemen yang lebih dahulu masuk ke setiap kelompok. Karena anggota kelompok berupa list, hasil groupBy dapat langsung diproses lagi oleh metode pipeline lain pada tingkat kelompok.

groovy Contoh: mengelompokkan skor dengan groupBy
def skor = [90, 75, 88, 70]
def kelompok = skor.groupBy { it >= 80 ? 'tinggi' : 'sedang' }
println kelompok
println kelompok['tinggi']
println kelompok.getClass().simpleName
text Keluaran
[tinggi:[90, 88], sedang:[75, 70]]
[90, 88]
LinkedHashMap

Akses kelompok['tinggi'] langsung menghasilkan list anggota kelompok tersebut. Apabila sebuah kunci kelompok tidak memiliki anggota sama sekali, kuncinya tidak akan muncul di dalam map hasil, sehingga pembacaan kunci yang tidak terdaftar menghasilkan null. Konsekuensinya, kelompok yang mungkin kosong perlu diperiksa dengan containsKey terlebih dahulu sebelum anggotanya diolah.

Metode inject menggabungkan seluruh elemen menjadi satu nilai dengan membawa akumulasi dari elemen ke elemen. Bentuk dengan argumen pertama menetapkan nilai awal akumulasi, sedangkan bentuk tanpa argumen memakai elemen pertama sebagai nilai awalnya dan mulai menggabungkan dari elemen kedua. Empat metode lain pada topik ini mengembalikan koleksi, sedangkan inject justru menutup rantai dengan satu nilai akhir. Nilai awal yang ditetapkan juga menentukan tipe akumulasi, misalnya 0 untuk penjumlahan bilangan bulat atau [] untuk pembangunan list baru.

groovy Contoh: inject dengan dan tanpa nilai awal
def skor = [90, 75, 88, 70]
def total = skor.inject(0) { akumulasi, n -> akumulasi + n }
println total
def gabung = ['Groovy', 'mudah', 'dibaca'].inject { a, b -> "$a $b" }
println gabung
text Keluaran
323
Groovy mudah dibaca

Nilai 323 diperoleh dari 0 + 90 + 75 + 88 + 70. Pola akumulasi yang sama berlaku untuk penggabungan string, penghitungan rata-rata, maupun pembangunan map baru. Satu catatan perilaku penting: bentuk tanpa nilai awal melempar NoSuchElementException apabila koleksinya kosong, karena tidak ada elemen yang dapat menjadi akumulasi pertama.

Mengurutkan dengan sort#

Metode sort tanpa argumen mengurutkan elemen mengikuti urutan alaminya. Dengan closure satu parameter, urutan ditentukan dari nilai kembalian closure, sedangkan closure dua parameter membandingkan dua elemen dan lazim memakai operator pesawat luar angkasa (<=>), yaitu operator perbandingan yang mengembalikan nilai negatif, nol, atau positif. Pada closure dua parameter, hasil negatif menempatkan elemen pertama di depan, hasil nol menyatakan keduanya setara, dan hasil positif menempatkan elemen kedua di depan. Membalik kedua operand, misalnya menulis b <=> a alih-alih a <=> b, otomatis membalik urutan hasilnya.

groovy Contoh: sort dengan operator pesawat luar angkasa
def kata = ['kiwi', 'jeruk', 'apel']
kata.sort { a, b -> a.length() <=> b.length() }
println kata
text Keluaran
[kiwi, apel, jeruk]

Perbedaan yang menentukan adalah sort memutasi list yang memanggilnya, sedangkan toSorted mengurutkan salinan dan membiarkan list aslinya utuh. Mutasi ini mudah luput karena nama metodenya tidak menunjukkan efek samping, sehingga list yang masih dipakai bagian kode lain dapat berubah urutan tanpa jejak. Pilih toSorted apabila urutan asli masih diperlukan di tahap lain, misalnya ketika list yang sama diurutkan dengan dua kriteria berbeda untuk dua ringkasan. Bentuk sort tetap tepat ketika list itu hanya dipakai satu tahap dan tidak dibagikan.

groovy sort-mutasi.groovy
// sort memutasi list asli, toSorted menyalin dahulu
def asli = [90, 75, 88, 70]
def salinan = asli.toSorted()
println "asli    : $asli"
println "salinan : $salinan"

def urut = [90, 75, 88, 70]
urut.sort()
println "urut    : $urut"
text Keluaran
asli    : [90, 75, 88, 70]
salinan : [70, 75, 88, 90]
urut    : [70, 75, 88, 90]

Menyusun tahapan menjadi pipeline#

Kelima metode di atas menjadi kuat ketika dirangkai, karena setiap tahap mengembalikan koleksi yang siap dipakai tahap berikutnya. Aliran datanya bergerak dari kiri ke kanan, mengikuti urutan penulisan tahapan pada rantai pemanggilannya. Berkas berikut mengolah daftar hasil ujian menjadi empat ringkasan, yaitu daftar yang lulus, anggota kelas tertentu, rata-rata nilai, dan urutan peringkat.

groovy pipeline-lengkap.groovy
// Pipeline nilai ujian: menyaring, mengubah, mengelompokkan, mengurutkan
def ujian = [
    [nama: 'Ali',   kelas: 'A', nilai: 90],
    [nama: 'Budi',  kelas: 'B', nilai: 75],
    [nama: 'Citra', kelas: 'A', nilai: 88],
    [nama: 'Dewi',  kelas: 'B', nilai: 70],
]

def lulus = ujian.findAll { it.nilai >= 80 }
println "Lulus     : ${lulus.collect { it.nama }}"

def perKelas = ujian.groupBy { it.kelas }
println "Kelas A   : ${perKelas['A'].collect { it.nama }}"

def total = ujian.collect { it.nilai }.inject(0) { akumulasi, n -> akumulasi + n }
println "Rata-rata : ${total / ujian.size()}"

def peringkat = ujian.sort { a, b -> b.nilai <=> a.nilai }.collect { it.nama }
println "Peringkat : $peringkat"
text Keluaran
Lulus     : [Ali, Citra]
Kelas A   : [Ali, Citra]
Rata-rata : 80.75
Peringkat : [Ali, Citra, Budi, Dewi]

Tahapan pada berkas tersebut memperlihatkan pola kerja yang dapat dibaca sebagai satu kalimat, misalnya sort { ... }.collect { it.nama } yang berarti mengurutkan dahulu, lalu mengambil namanya. Nilai rata-rata 80.75 muncul sebagai bilangan desimal karena operator pembagian Groovy menghasilkan BigDecimal untuk operand bilangan bulat. Perlu dicatat bahwa baris peringkat memanggil sort yang memutasi ujian, sehingga urutan daftar itu berubah menjadi terurut menurun setelah baris tersebut dijalankan. Apabila urutan aslinya masih dibutuhkan, toSorted pada tahap peringkat adalah pilihan yang lebih aman.

Kesalahan umum#

Kesalahan pertama adalah menganggap hasil collect pada map sebagai map. Metode collect selalu menghasilkan list, dan closure satu parameter menerima objek Entry yang kuncinya diakses melalui properti key. Apabila bentuk map tetap diinginkan, findAll adalah metode yang tepat karena hasil penyaringan pada map memang tetap berupa map.

groovy Contoh: collect pada map menghasilkan entri
def nilai = [Ali: 90, Budi: 75]
def entri = nilai.collect { it }
println entri
println entri[0].getClass().simpleName
println entri*.key
text Keluaran
[Ali=90, Budi=75]
Entry
[Ali, Budi]

Kesalahan kedua adalah memanggil inject tanpa nilai awal pada koleksi yang kosong. Karena tidak ada elemen yang dapat menjadi akumulasi pertama, Groovy melempar NoSuchElementException. Bentuk inject(0) yang selalu menyediakan nilai awal terhindar dari kegagalan ini.

groovy Contoh: inject tanpa nilai awal pada list kosong
try {
    [].inject { a, b -> a + b }
} catch (NoSuchElementException e) {
    println "Gagal: ${e.getClass().simpleName}"
}
text Keluaran
Gagal: NoSuchElementException

Ringkasan#

  • findAll menyaring tanpa mengubah koleksi asli, dan pada map hasilnya tetap berupa map.
  • collect mengubah setiap elemen menjadi list baru, sedangkan groupBy menghasilkan LinkedHashMap yang nilainya berupa list.
  • inject menumpuk elemen menjadi satu nilai, dan bentuk tanpa nilai awal gagal pada koleksi kosong.
  • sort memutasi list yang memanggilnya, sedangkan toSorted menghasilkan salinan terurut.

Referensi#