≡ Menu

Dive in Clojure: Abstraksi yang Lebih Abstrak dengan Transducer!

Transducer

Sudah lebih dari seminggu, saya benar-benar tertarik dengan om.next. om.next adalah react wraper untuk clojurescript. Ketika saya mengikuti tutorial om.next, ada satu baris kode yang membuat saya bingung. Saya benar-benar bingung!

(defn get-people [state key]
  (let [st @state]
    (into [] (map #(get-in st %)) (get st key))))

Saya pikir kode pada baris ke-3 salah, (map #(get-in st %)), ternyata saya yang keliru. Selama ini form dari map yang saya tahu adalah (map f data ..). Ketika buka kembali dokumentasi dari map:

Usage: (map f)
       (map f coll)
       (map f c1 c2)
       (map f c1 c2 c3)
       (map f c1 c2 c3 & colls)

Kode (map #(get-in st %)) adalah kode yang valid untuk form (map f). Ketika saya invoke kode ini me-return fungsi transducer?????

The hack is transducer?!

Saya googling, dan menemukan jawaban dari Rich Hickey langsung:

Transducers are a powerful and composable way to build algorithmic transformations that you can reuse in many contexts …

Saya tambah bingung! lah kalo jawabannya itu, apa bedanya sama fungsi? bukankan fungsi juga seperti itu.

Oke, tulisan di bawah ini mungkin salah! jadi tolong koreksi saya. Pada tulisan ini dibuat, saya baru tiga hari mempelajari transducer.

The Problem Trying to Solve

Yang membedakan dari transducer dan fungsi biasa, transducer hanya menerima satu argumen yang berupa fungsi dan selalu mengembalikan transducer lagi. Sedangkan fungsi biasa menerima nol argumen atau lebih, argumen bisa apa saja, dan mengembalikan invoke f terhadap argumen.

Spesial untuk transducer, argumen yang diterima hanyalah reducing function!

Reducing function adalah fungsi yang diterima fungsi reduce, menerima input so-far dan mengembalikan input selanjutnya. Contoh reducing function adalah + dan conj.

Di dunia functional programming, reduce biasa juga disebut fold.

;; jumlahkan semua item dalam list
(reduce + (range 100))
;;=> 4950
 
;; masukan semua list dalam vector
(reduce conj [] (range 100))
;;=> [0 1 2 3 ... 99]

Problemnya gak asik yah, tapi intinya gitu, fungsi reducing adalah fungsi yang diterima di fungsi reduce. Contoh-contoh fungsi reduce bisa dilihat clojuredocs.

;; Reducing vs Transducer

;; reducing function signature
whatever, input -> whatever

;; transducer signature
(whatever, input -> whatever) -> (whatever, input -> whatever)

Masalah yang coba diselesaikan transducer adalah generalisasi, membuat fungsi yang lebih abstrak. Transducer memodelkan langkah-langkah yang diambil untuk memproses sesuatu (apa saja), dengan tidak peduli detail.

Transducer with map & filter

Fungsi yang sangat sering digunakan, map dan filter, adalah ide kenapa transducer ada. Dulunya di Clojure, map dan filter walaupun dengan proses yang sama, tetapi tidak reusable-friendly. Akhirnya setiap list sesuatu memiliki versi map/filter-nya masing-masing.

- MyCollection -> MyCollection
- Stream -> Stream
- Channel -> Channel
- Observable -> Observable

Untuk memahami transducer kita akan membuat versi map dan filter dengan fungsi reduce.

(defn mapl [f coll]
  (reduce (fn [r x] (conj r (f x)))
          [] coll))
 
(mapl inc (range 10))
;;=> [1 2 3 4 5 6 7 8 9 10]
 
(defn filterl [pred coll]
  (reduce (fn [r x] (if (pred x) (conj r x) r))
          [] coll))
 
(filterl odd? (range 10))
;;=> [1 3 5 7 9]

Sampai di sini, kita bisa melihat problem dari kode di atas. Kode di atas terlalu spesifik dan detail, mapl dan filterl tahu implementasi conj!

Dengan ada conj kode di atas menjadi tidak fleksibel, kode di atas hanya menerima argumen yang valid untuk conj saja.

Oke, kita akan membuatnya lebih abstrak.

(defn mapping [f]
  (fn [step]
    (fn [r x] (step r (f x)))))
 
(((mapping inc) conj) [] 1)
;;=> [1]
 
(reduce ((mapping inc) conj) [] (range 10))
;;=> [1 2 3 4 5 6 7 8 9 10]
 
(defn filtering [pred]
  (fn [step]
    (fn [r x] (if (pred x) (step r x) r))))
 
(((filtering odd?) conj) [] 1)
;;=> [1]
 
(reduce ((filtering odd?) conj) [] (range 10))
;;=> [1 3 5 7 9]

Bukti dari kode di atas lebih fleksibel dan abstrak, kita bisa mengganti conj dengan +.

(reduce ((mapping inc) +) (range 10))
;;=> 54
 
(reduce ((filtering odd?) +) (range 10))
;;=> 25

Fungsi mapping dan filtering keduanya tidak tahu detail. Yang mereka tahu hanyalah bagaimana menggunakan argimennya untuk melakukan pekerjaan mereka.

Karena ((mapping inc) +) dan ((filtering odd?) +) memenuhi fungsi reduce, kedua fungsi itu adalah reducing function!

Bagaiman jika kita invoke (mapping inc) dan (filtering odd?) saja? apa yang terjadi? mereka hanya mengembalikan fungsi! Dan fungsi yang dikembalikan menerima argumen yang sama, yaitu step. (mapping inc) dan (filtering odd?) inilah yang kita sebut sebagai transducer!

Karena transducer mengembalikan fungsi yang menerima argumensama, yaitu step kita dapat menggabungkan kedua fungsi tersebut sebagai suksesor! Bayangkan sebagai mesin yang digabungkan dan menghasilkan mesin baru. Di clojure kita menggunakan fungsi comp.

(def xform
  (comp
    (mapping inc)
    (filtering odd?)))

xform adalah transdicer juga, karena menghasilkan “mesin” baru! ingat bahwa transducer:

;;transducer signature
(whatever, input -> whatever) -> (whatever, input -> whatever)

Kita tahu bahwa invoke (filtering odd?) akan mendapatkan transduce! jika kita tambahkan implementasi detail ((filtering odd?) conj) yang kita dapatkan adalah reducing function! Ingat bahwa reducing function menerima input-so-far dan mengembalikan input selanjutnya,

;; reducing function signature
whatever, input -> whatever

Sampai di sini, seharusnya kita sudah bisa membedakan fungsi biasa dengan fungsi transducer. Sama seperti manusia, trasducer memandang fungsi sebagai mesin black-box.

jika saya meng-invoke (reduce (xform conj) [] (range 10)) apa hasilnya?

Jika jawaban kamu adalah [2 4 6 8 10], kamu masih memandang transducer sebagai fungsi biasa, bukan transducer sebagai transducer. Kamu memandang transducer menghasilkan value, bukan transducer yang menghasilkan transducer.

Transducible Processes

Fungsi apa saja yang menerima transducer?

;; sequence
(sequence (map inc) (range 10))
;;=> (1 2 3 4 5 6 7 8 9 10)

;; into
(into [] (map inc) (range 10))
;;=> [1 2 3 4 5 6 7 8 9 10]

;; transduce
(transduce (map inc) [] (range 10))
;;=> [1 2 3 4 5 6 7 8 9 10]

dll..

Sekarang, kembali ke pertanyaan awal saya. Fungsi (map #(get-in st %)) adalah transducer!

(def st {:list/one
         [[:person/by-name "John"]
          [:person/by-name "Mary"]
          [:person/by-name "Bob"]],
         :list/two
         [[:person/by-name "Mary"]
          [:person/by-name "Gwen"]
          [:person/by-name "Jeff"]],
         :person/by-name
         {"John" {:name "John", :points 0},
          "Mary" {:name "Mary", :points 0, :age 27},
          "Bob" {:name "Bob", :points 0},
          "Gwen" {:name "Gwen", :points 0},
          "Jeff" {:name "Jeff", :points 0}}})
 
(into [] (map #(get-in st %)) (get st :list/one))
 
;;=> [{:name "John", :points 0}
;;    {:name "Mary", :points 0, :age 27}
;;    {:name "Bob", :points 0}]

It’s fun, isn’t it? sekarang kita dapat membuat reusable function identik untuk input apa saja! bukan membuat parameterized function!

So far, lagi-lagi Clojure membukakan mata saya terhadap cara berpikir baru yang belum pernah saya pelajari di bahasa mainstream, saya masih ingat bagaimana meng-handle loop dengan recursive pattern, selanjutnya thinking in functional programming, dan kali ini abstraksi yang lebih abstrak melalui transducer.

Referensi:
1. http://blog.cognitect.com/blog/2014/8/6/transducers-are-coming
2. http://elbenshira.com/blog/understanding-transducers/
3. http://conscientiousprogrammer.com/blog/2014/08/07/understanding-cloure-transducers-through-types/
4. https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/Transducers.md
5. http://isaaccambron.com/blog/2014/12/13/transducer-composition.html

{ 0 comments… add one }

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.