Pipeline transformasi data di Clojure tersusun atas tiga fungsi inti, yaitu filter untuk seleksi, map untuk konversi, dan reduce untuk perangkuman. Topik ini menyusun ketiganya menjadi pipeline yang utuh atas data nilai ujian dan transaksi loket, menunjukkan keluaran nyata pada setiap tahap, serta membandingkan bentuk bertingkat dengan threading macro secara ringkas.
Seleksi dan konversi dengan filter dan map#
filter menerima fungsi predikat, yaitu fungsi yang mengembalikan nilai benar atau salah, dan menyisakan elemen yang lolos predikat tersebut. map menerima fungsi transformasi dan menerapkannya pada setiap elemen. Keduanya mengembalikan lazy sequence, yaitu seq yang elemennya baru dihitung saat dibaca, sehingga penggabungan keduanya tetap murah karena tidak ada koleksi antara yang dibangun. Perilaku lazy sequence itu dibahas tersendiri pada topik Lazy Sequence.
Kode berikut bekerja pada delapan nilai ujian dan mengonversinya menjadi huruf.
(def nilai [78 55 92 67 88 45 73 81])
(defn konversi [n]
(cond
(>= n 85) "A"
(>= n 75) "B"
(>= n 65) "C"
:else "D"))
(prn (filter #(>= % 70) nilai))
(prn (map konversi nilai))
(prn (map konversi (filter #(>= % 70) nilai)))(78 92 88 73 81)
("B" "D" "A" "C" "A" "D" "C" "B")
("B" "A" "A" "C" "B")Bentuk #(>= % 70) adalah fungsi tanpa nama (anonymous function) dengan % sebagai satu-satunya argumennya, sedangkan konversi menggunakan cond untuk memilih huruf menurut ambang batas. Fungsi tanpa nama digunakan ketika predikat cukup pendek untuk ditulis sebaris. Keluaran pertama menyisakan lima nilai yang lulus, keluaran kedua memperlihatkan konversi seluruh nilai, dan keluaran ketiga menggabungkan keduanya sehingga hanya nilai lulus yang dikonversi.
reduce dan akumulator#
reduce merangkum sebuah seq menjadi satu nilai dengan menjalankan fungsi dua argumen secara berulang. Argumen pertama fungsi tersebut adalah akumulator (accumulator), yaitu nilai sementara yang dibawa dari langkah sebelumnya, sedangkan argumen kedua adalah elemen berikutnya. Apabila nilai awal tidak disediakan, elemen pertama digunakan sebagai akumulator awal.
(println (reduce + [78 55 92 67 88 45 73 81]))
(println (reduce + 1000 [1 2 3]))
(println (reduce max [3 91 17 42]))579
1006
91Baris pertama menjumlahkan seluruh nilai ujian menjadi 579. Baris kedua menggunakan bentuk tiga argumen dengan nilai awal 1000, sehingga hasilnya 1006. Baris ketiga menggunakan max sebagai fungsi reduksi sehingga menghasilkan nilai tertinggi, yaitu 91.
Jalannya akumulator terbaca dari satu langkah ke langkah berikutnya. Pada (reduce + 1000 [1 2 3]), akumulator dimulai dari 1000, menjadi 1001 setelah elemen 1, menjadi 1003 setelah elemen 2, dan berhenti di 1006. Tanpa nilai awal, elemen pertama menggantikan posisi akumulator awal, sehingga langkah pertamanya melibatkan dua elemen sekaligus.
Rata-rata kemudian diperoleh dengan memadukan reduce dan count.
(def nilai [78 55 92 67 88 45 73 81])
(println (/ (reduce + nilai) (count nilai)))579/8Pembagian 579 dengan 8 menghasilkan 579/8, yaitu rasio (ratio) yang dihitung secara eksak, bukan pembulatan desimal. Clojure mempertahankan presisi dengan menggunakan rasio ketika dua bilangan bulat tidak habis dibagi. Konversi ke bentuk desimal dilakukan dengan fungsi double apabila bentuk itulah yang diinginkan.
Pipeline atas transaksi loket#
Pipeline yang sama berlaku atas data berstruktur map. Data transaksi berikut memuat empat catatan loket, dan dua pipeline di bawahnya menghitung total pendapatan serta pendapatan Loket A saja.
(def transaksi
[{:lokasi "Loket A" :jumlah 15000}
{:lokasi "Loket B" :jumlah 22000}
{:lokasi "Loket A" :jumlah 9000}
{:lokasi "Loket C" :jumlah 31000}])
(println (reduce + (map :jumlah transaksi)))
(println (reduce + (map :jumlah (filter #(= "Loket A" (:lokasi %)) transaksi))))77000
24000Ekspresi (map :jumlah transaksi) mengambil nilai kunci :jumlah dari setiap entri, kemudian reduce menjumlahkannya menjadi 77000. Pipeline kedua menyisipkan filter yang menyisakan transaksi Loket A sebelum penjumlahan, sehingga hasilnya 24000.
Setiap tahap pipeline menerima dan mengembalikan seq, sehingga tahap baru dapat disisipkan tanpa menulis ulang tahap yang sudah ada. Seleksi kedua, misalnya hanya nilai genap, cukup diselipkan pada posisi yang tepat, dan fungsi reduksinya tidak berubah. Sifat komposisi inilah yang membuat gaya pipeline tumbuh dari pipeline kecil menjadi alur data yang panjang tanpa kehilangan keterbacaan.
Bentuk bertingkat (nesting) di atas dibaca dari dalam ke luar, yaitu filter lebih dahulu, lalu map, kemudian reduce. Threading macro seperti ->> menata ulang bentuk tersebut agar terbaca dari atas ke bawah dan dibahas tersendiri pada topik threading macro. Untuk pipeline pendek, bentuk bertingkat tetap wajar digunakan. Sebagai pembanding, jumlah nilai lulus beserta hurufnya dapat dihitung dari pipeline yang serupa.
(def nilai [78 55 92 67 88 45 73 81])
(defn konversi [n]
(cond
(>= n 85) "A"
(>= n 75) "B"
:else "D"))
(prn (map konversi (filter #(>= % 70) nilai)))
(println (count (filter #(>= % 70) nilai)))("B" "A" "A" "D" "B")
5count atas hasil filter menghitung lima nilai lulus, sedangkan map mengonversi kelima nilai tersebut menjadi huruf. Ketiga fungsi bekerja dalam satu gaya yang sama: data mengalir dari satu transformasi ke transformasi berikutnya tanpa variabel perantara, dan setiap tahap dapat diuji secara terpisah karena menerima dan mengembalikan seq.
Kesalahan umum#
- Menggunakan
reducetanpa nilai awal atas koleksi kosong. Fungsi reduksi dipanggil tanpa argumen sehingga umumnya melempar exception, contohnyamaxatas vector kosong. Nilai awal yang tepat, misalnya 0 untuk penjumlahan, mencegah masalah ini. - Menggunakan
filteruntuk mengubah nilai.filterhanya memilih elemen melalui predikatnya, sedangkan transformasi nilai adalah tugasmap. Menyisipkan logika konversi ke dalam predikat membuat kode sulit diuji. - Mengikat hasil
mapataufilterdengandeflalu menganggap perhitungan sudah selesai. Hasil keduanya adalah lazy sequence, sehingga perhitungan baru berjalan saat hasilnya dibaca. Konsumsi langsung denganreduceatau kumpulkan denganintodanmapvbila hasil konkret diperlukan.
Ringkasan#
filtermenyaring elemen dengan predikat,mapmengubah setiap elemen, danreducemerangkum seluruh seq menjadi satu nilai melalui akumulator.- Nilai awal pada
reducemenentukan perilaku atas koleksi kosong dan membuat pipeline lebih aman. - Pembagian bilangan bulat menghasilkan rasio yang eksak, seperti
579/8, dandoublemengubahnya menjadi desimal. - Bentuk bertingkat dibaca dari dalam ke luar, sedangkan threading macro menata ulang pipeline yang panjang.
Referensi#
- clojure.core/map — ClojureDocs — dokumentasi dan contoh penggunaan map beserta catatan perilaku lazy-nya.
- clojure.core/filter — ClojureDocs — dokumentasi filter beserta contoh predikat yang lazim digunakan.
- clojure.core/reduce — ClojureDocs — dokumentasi reduce beserta contoh nilai awal dan fungsi reduksi.