Transformasi data pada Clojure lazim ditulis sebagai rantai map dan filter yang menerima koleksi secara langsung. Bentuk tersebut nyaman digunakan, tetapi setiap langkah terkopling ke sumber datanya sehingga pipeline yang sama sulit dipindahkan ke konteks lain. Topik ini membahas transducer, yakni transformasi yang disusun tanpa koleksi dan diterapkan belakangan pada konteks penerapan mana pun. Setelah topik ini dibaca, satu definisi pipeline dapat digunakan ulang untuk vector, lazy sequence, maupun reduksi tanpa menyalin logikanya.
Transformasi tanpa koleksi#
map yang dipanggil dengan fungsi transformasi saja, tanpa koleksi, tidak menghasilkan seq melainkan sebuah transducer. Transducer tidak menyentuh satu elemen pun pada saat didefinisikan karena sumber datanya belum ditentukan. Ia hanya mendeskripsikan satu langkah pemrosesan, yaitu cara mengubah satu elemen menjadi nilai berikutnya di dalam aliran. Konteks penerapan disediakan belakangan, misalnya oleh sequence yang menghasilkan lazy sequence, atau oleh into yang mengumpulkan hasil ke koleksi eager, yakni koleksi yang dihitung segera secara penuh.
;; (map f) tanpa koleksi menghasilkan transducer
(def kali-10 (map #(* % 10)))
(println "sequence :" (sequence kali-10 [1 2 3 4]))
(println "into :" (into [] kali-10 [1 2 3 4]))sequence : (10 20 30 40)
into : [10 20 30 40]Pada contoh di atas, kali-10 adalah transducer hasil pemanggilan map tanpa koleksi. sequence menerapkannya atas [1 2 3 4] dan menghasilkan lazy sequence berisi (10 20 30 40), sedangkan into mengumpulkan hasil pemrosesan yang sama ke dalam vector. Kedua konteks itu menggunakan satu definisi transformasi yang identik, dan tidak ada elemen yang diproses sebelum konteksnya meminta. Perilaku ini berbeda dari (map f koleksi), yang segera bekerja atas data yang disebutkan pada baris yang sama.
Anatomi fungsi langkah#
Setiap transducer berupa fungsi yang menerima satu fungsi langkah (reducing function) dan mengembalikan fungsi langkah baru. Fungsi langkah adalah fungsi yang mengakumulasi hasil, dan dokumentasi resmi menetapkan tiga bentuk parameter (arity) yang wajib dijaga. Arity tanpa argumen meminta nilai awal, arity satu argumen menyelesaikan akumulasi, dan arity dua argumen memproses satu elemen. Transducer menulis ulang arity dua argumen tersebut, sedangkan dua arity lainnya diteruskan apa adanya ke fungsi langkah yang diterimanya.
;; transducer ditulis tangan: fungsi dari fungsi langkah ke fungsi langkah
(def kali-10
(fn [rf]
(fn
([] (rf))
([acc] (rf acc))
([acc x] (rf acc (* 10 x))))))
(println "(transduce kali-10 + [1 2 3]) =" (transduce kali-10 + [1 2 3]))
(println "(sequence (map inc) [1 2 3]) =" (sequence (map inc) [1 2 3]))(transduce kali-10 + [1 2 3]) = 60
(sequence (map inc) [1 2 3]) = (2 3 4)kali-10 di atas membungkus fungsi langkah rf menjadi versi baru yang mengalikan setiap elemen dengan 10 sebelum meneruskannya. Pemanggilan (transduce kali-10 + [1 2 3]) menjalankan alur tersebut sampai akumulasi akhir, sehingga hasilnya 60. Bentuk ini persis seperti isi (map f) pada core, hanya logika langkahnya yang berbeda. Karena transducer semata-mata menata ulang pemanggilan fungsi langkah, sumber elemen tidak pernah disebut di dalamnya, dan itulah dasar sifat bebas konteksnya.
Menyusun transducer dengan comp#
comp juga menjadi alat penyusun transducer, dengan satu perbedaan urutan yang penting. Pada komposisi fungsi biasa, (comp f g) memanggil g lebih dahulu. Pada komposisi transducer, (comp xf1 xf2) menerapkan xf1 lebih dahulu atas setiap elemen, sehingga urutan penerapannya dibaca dari kiri ke kanan. Alasannya mekanis, yaitu setiap transducer membungkus fungsi langkah yang berada di kanannya, sehingga elemen masuk melalui pembungkus paling kiri terlebih dahulu sebelum mengalir ke kanan.
(def nilai [80 85 78 92 88 74 95 69])
;; urutan comp pada transducer: kiri dikerjakan lebih dahulu
(def olah
(comp (filter #(>= % 80))
(map #(/ % 25.0))))
(println "into :" (into [] olah nilai))
(println "sequence :" (sequence olah nilai))into : [3.2 3.4 3.68 3.52 3.8]
sequence : (3.2 3.4 3.68 3.52 3.8)Pipeline olah menyaring nilai ujian minimal 80, kemudian mengubah nilai yang lolos ke skala 4 melalui pembagian dengan 25.0. Apabila urutannya dibalik, map akan mengubah seluruh nilai lebih dahulu, dan hasilnya tetap benar untuk contoh ini, tetapi pekerjaan pembagian dilakukan atas delapan elemen alih-alih lima elemen yang lolos saringan. Pada pipeline yang lebih panjang, urutan kiri ke kanan yang tepat mengurangi beban setiap langkah berikutnya secara nyata.
Penerapan lewat into, sequence, dan transduce#
Tiga fungsi penerapan transducer pada core adalah into, sequence, dan transduce. into mengumpulkan hasil ke koleksi eager seperti vector, sequence menghasilkan lazy sequence yang direalisasikan sebagaimana diminta, sedangkan transduce menjalankan seluruh pipeline sampai akumulasi akhir dengan fungsi reduksi yang disediakan. Fungsi reduksi lazimnya +, max, atau min. Satu hal perlu diperhatikan, yaitu transduce meminta nilai awal secara eksplisit apabila fungsi reduksinya tidak memiliki arity tanpa argumen, seperti max yang menuntut minimal satu angka.
(def nilai [80 85 78 92 88 74 95 69])
(println "total nilai lulus :" (transduce (filter #(>= % 80)) + nilai))
(println "nilai lulus tertinggi:" (transduce (filter #(>= % 80)) max 0 nilai))total nilai lulus : 440
nilai lulus tertinggi: 95Bentuk pertama menjumlahkan seluruh nilai yang lolos saringan dengan +, yang memang memiliki arity tanpa argumen berupa 0, sehingga nilai awal tidak perlu ditulis. Bentuk kedua mencari nilai tertinggi dengan max dan menyediakan 0 sebagai nilai awalnya. Kedua pemanggilan menyelesaikan pekerjaan dengan satu kali penelusuran atas nilai, tanpa membentuk koleksi sementara berisi hasil saringan di antara langkah-langkahnya.
Perbandingan dengan pipeline seq#
Versi seq membangun struktur perantara pada setiap langkahnya. Ekspresi (filter pred nilai) menghasilkan satu lazy sequence, lalu (map f ...) di atasnya menghasilkan lazy sequence kedua, dan into akhirnya menyalin hasilnya ke vector. Elemen yang sama melewati beberapa kali traversal, dan memori terpakai untuk struktur perantara tersebut. Versi transducer tidak membentuk struktur antara apa pun karena elemen mengalir satu per satu melalui seluruh fungsi langkah yang tersusun, sehingga traversalnya tunggal dari awal sampai akhir.
(def nilai [80 85 78 92 88 74 95 69])
;; versi seq: filter dan map masing-masing menghasilkan seq antara
(def via-seq
(into [] (map #(/ % 25.0) (filter #(>= % 80) nilai))))
;; versi transducer: satu pipeline tanpa seq antara
(def via-transducer
(into [] (comp (filter #(>= % 80)) (map #(/ % 25.0))) nilai))
(println "via-seq :" via-seq)
(println "via-transducer :" via-transducer)via-seq : [3.2 3.4 3.68 3.52 3.8]
via-transducer : [3.2 3.4 3.68 3.52 3.8]Kedua versi menghasilkan vector yang sama persis, sehingga pilihan di antara keduanya adalah perkara struktur kerja, bukan perkara hasil. Karena transducer tidak terkopling ke koleksi, definisi pipeline yang sama dapat diteruskan ke into, sequence, maupun transduce tanpa penulisan ulang. Sifat yang sama pula menjadikan transducer cocok untuk konteks di luar koleksi, misalnya channel pada core.async atau Java stream, sebab keduanya juga meneruskan elemen satu per satu.
Kesalahan umum#
Kesalahan pertama berupa urutan comp yang terbalik. Kebiasaan membaca komposisi fungsi biasa dari kanan ke kiri membuat (comp (map f) (filter p)) tertulis, padahal maksudnya menyaring lebih dahulu. Pada transducer, urutan penerapan dibaca kiri ke kanan, sehingga filter harus berada di sebelah kiri. Pipeline yang baru disusun sebaiknya diuji dengan satu nilai sederhana sebelum dipasangkan ke data nyata.
Kesalahan kedua berupa transducer yang dipanggil langsung atas data. Pemanggilan ((map inc) 5) memicu error karena transducer menuntut sebuah fungsi langkah sebagai argumennya, bukan elemen. Untuk memproses data, sambungkan transducer ke konteksnya terlebih dahulu, misalnya melalui sequence atau into.
Kesalahan ketiga berkaitan dengan nilai awal pada transduce.
Ringkasan#
(map f)tanpa koleksi menghasilkan transducer, yaitu transformasi yang bebas konteks dan tidak menyentuh data pada saat didefinisikan.- Transducer berupa fungsi dari fungsi langkah ke fungsi langkah, dengan tiga arity yang menjaga inisialisasi, penyelesaian, dan langkah pemrosesan.
compmenyusun transducer dengan urutan penerapan kiri ke kanan, berbeda dari komposisi fungsi biasa.into,sequence, dantransducemenerapkan pipeline yang sama atas vector, lazy sequence, dan reduksi dalam satu kali traversal.
Referensi#
- Transducers — Clojure.org — Referensi resmi transducer, termasuk kontrak fungsi langkah dan urutan komposisinya.
- transduce — ClojureDocs
— Dokumentasi
transducebeserta contoh penggunaan nilai awalnya.