Kefasihan Threading Macro

Menata aliran data dengan threading macro thread-first, thread-last, as->, dan some-> dalam praktik.

Daftar isi

Ekspresi Clojure dituliskan bersarang, sehingga pemanggilan yang terjadi paling awal terbaca paling dalam. Threading macro menata ulang ekspresi bersarang tersebut menjadi pipeline yang terbaca dari atas ke bawah mengikuti urutan eksekusinya. Topik ini membahas -> untuk penyisipan pada posisi pertama, ->> untuk posisi terakhir, as-> untuk penempatan bebas, dan some-> untuk rantai yang berhenti pada nil, dengan data pesanan sebagai contoh pemrosesannya.

Dari bersarang ke pipeline dengan -»#

Kode bersarang menuntut pembacaan dari dalam ke luar. Ekspresi (reduce + (map :harga (filter :terkirim pesanan))) dieksekusi mulai dari filter, lalu map, lalu reduce, tetapi terbaca dalam urutan sebaliknya. ->> menyisipkan nilai sebagai argumen terakhir pada setiap form berikutnya, sehingga aliran data menurun searah dengan pembacaan.

Ekspansi (->> pesanan (filter :terkirim) (map :harga) (reduce +)) adalah (reduce + (map :harga (filter :terkirim pesanan))). Fungsi filter dan map menerima koleksi pada argumen terakhirnya, sehingga pola pemanggilan ini sejalan dengan ->>.

clojure Total pesanan terkirim: versi bersarang dan threading
(def pesanan
  [{:produk "Buku" :harga 55000 :terkirim true}
   {:produk "Pulpen" :harga 8000 :terkirim false}
   {:produk "Binder" :harga 22000 :terkirim true}])

(defn total-bersarang
  [pesanan]
  (reduce + (map :harga (filter :terkirim pesanan))))

(defn total-threading
  [pesanan]
  (->> pesanan
       (filter :terkirim)
       (map :harga)
       (reduce +)))

(println "Versi bersarang :" (total-bersarang pesanan))
(println "Versi threading :" (total-threading pesanan))
text Keluaran
Versi bersarang : 77000
Versi threading : 77000

Kedua fungsi menghasilkan 77000, yaitu jumlah harga butir yang berstatus terkirim. Versi threading menyatakan tahapan yang sama tanpa kurung bertingkat, sehingga penambahan tahap baru cukup dilakukan dengan satu baris form, dan setiap tahap terbaca sebagai langkah berikutnya dalam pipeline.

Thread-first dengan ->#

-> menyisipkan nilai sebagai argumen pertama pada setiap form berikutnya. Ekspansi (-> pesanan (assoc :status "diproses") (update :total + 15000)) adalah (update (assoc pesanan :status "diproses") :total + 15000), karena pesanan disisipkan pada posisi pertama assoc, lalu hasilnya disisipkan pada posisi pertama update. Keyword yang dituliskan berdiri sendiri pada -> dipanggil sebagai fungsi atas nilai yang mengalir. Dengan demikian, (-> pesanan :pelanggan :nama) sama dengan (:nama (:pelanggan pesanan)).

clojure Membaca dan memperbarui pesanan dengan ->
(def pesanan {:kode "ORD-104"
              :pelanggan {:nama "Dewi" :kota "Bandung"}
              :total 175000})

(println (:nama (:pelanggan pesanan)))
(println (-> pesanan :pelanggan :nama))

(println (-> pesanan
             (assoc :status "diproses")
             (update :total + 15000)
             :total))
text Keluaran
Dewi
Dewi
190000

Fungsi assoc dan update mengharuskan map berada pada argumen pertama, sehingga -> adalah pilihan yang tepat untuk kelompok fungsi ini. Pipeline terakhir membaca map pesanan, menambahkan kunci :status, menaikkan :total dengan biaya tambahan 15000, lalu membaca nilai :total sebagai hasil akhirnya. Tahapan yang menyisipkan data ke posisi pertama dan tahapan yang membaca dengan keyword dapat dicampur dalam satu pipeline karena setiap form menyisipkan nilai sesuai posisinya masing-masing.

as-> untuk penempatan bebas#

Apabila posisi nilai berpindah antara argumen pertama, terakhir, maupun tengah, -> dan ->> tidak mencukupi. as-> mengikat nilai yang mengalir kepada sebuah nama, dan setiap form menuliskan nama itu pada posisi yang diinginkan. Tidak ada penyisipan otomatis, sehingga lupa menuliskan nama berarti mengabaikan nilai yang mengalir.

clojure Rantai as-> dengan posisi nilai yang berubah
(def nilai-uji [80 75 90 60])

(println
 (as-> nilai-uji data
   (filter #(>= % 75) data)
   (count data)
   (double (/ data 100))
   (str "Persentase kelulusan: " data "%")))
text Keluaran
Persentase kelulusan: 0.03%

Nama data berpindah bentuk dari vector menjadi seq hasil filter, lalu menjadi angka hasil count, menjadi double hasil pembagian, dan berakhir sebagai string. Pada form (double (/ data 100)), nilai yang mengalir berada pada posisi pembilang, posisi yang tidak dapat dicapai oleh ->> maupun ->. Bentuk as-> juga berguna apabila satu tahap berupa form yang tidak menerima penyisipan, karena penempatannya sepenuhnya eksplisit. Konsekuensinya, rantai yang terlalu panjang pada as-> lebih sulit dibaca daripada versi ->> atau ->, sehingga bentuk ini dipilih hanya ketika posisi memang berubah-ubah.

some-> untuk rantai yang dapat berhenti#

Data opsional seperti kupon mungkin tidak ada pada sebagian pesanan. Rantai akses yang dilanjutkan dengan operasi atas hasilnya berakhir error apabila salah satu tautannya nil, karena operasi berikutnya tidak menerima nil. some-> meneruskan nilai sebagaimana ->, tetapi berhenti dan mengembalikan nil pada hasil nil yang pertama.

clojure Membaca kupon dengan some->
(def pesanan-a {:kode "ORD-101" :kupon {:kode "DISKON10" :diskon 10}})
(def pesanan-b {:kode "ORD-102"})

(println "Kode kupon A :" (some-> pesanan-a :kupon :kode (.toUpperCase)))
(println "Diskon A     :" (some-> pesanan-a :kupon :diskon))
(println "Kode kupon B :" (some-> pesanan-b :kupon :kode (.toUpperCase)))
(println "Diskon B     :" (some-> pesanan-b :kupon :diskon))
text Keluaran
Kode kupon A : DISKON10
Diskon A     : 10
Kode kupon B : nil
Diskon B     : nil

Pada pesanan tanpa kunci :kupon, hasil nil langsung dijadikan nilai akhir dan sisa rantainya tidak dijalankan. Perilaku kontrasnya terlihat apabila -> digunakan untuk rantai yang sama.

clojure Rantai -> yang gagal pada nil
; tidak-dijalankan
; (.toUpperCase nil) melempar NullPointerException, sehingga versi ->
; berhenti dengan error ketika :kupon tidak ada
(println (-> {:kode "ORD-102"} :kupon :kode (.toUpperCase)))

Error yang muncul berupa NullPointerException karena metode .toUpperCase dipanggil atas nil, bukan karena pembacaan kuncinya keliru. Pemilihan some-> menyatakan perilaku itu secara eksplisit, yaitu rantai yang boleh berhenti di tengah tanpa memunculkan error.

Kesalahan umum#

Kesalahan pertama berupa pemilihan ->> untuk fungsi yang menerima data pada argumen pertama. (->> pesanan (assoc :status "diproses")) berekspansi menjadi (assoc :status "diproses" pesanan), yang menempatkan keyword pada posisi map sehingga menghasilkan error bertipe ClassCastException.

Kesalahan kedua berupa pembagian yang ditempatkan pada posisi yang salah. Pada nilai 5, ekspansi (-> 5 (/ 10)) menjadi (/ 5 10) yang menghasilkan 1/2, sedangkan (->> 5 (/ 10)) menjadi (/ 10 5) yang menghasilkan 2. Satu posisi penyisipan mengubah hasil pembagian sepenuhnya. Kesalahan ketiga berupa threading yang berlebihan, yaitu satu pipeline panjang dengan tahapan yang tidak benar-benar searah, sehingga lebih sulit diurai daripada beberapa let berlabel. Tahapan yang berbelok sebaiknya dipecah, dan penggantian some-> dengan -> pada data opsional juga dihindari agar rantai tidak gagal dengan error di tengah.

Ringkasan#

  • ->> menyisipkan nilai sebagai argumen terakhir dan paling cocok untuk filter, map, dan reduce yang menerima koleksi di ujung.
  • -> menyisipkan nilai sebagai argumen pertama dan menjadi pilihan untuk assoc, update, serta keyword yang dipanggil sebagai fungsi.
  • as-> mengikat nilai yang mengalir ke sebuah nama, sehingga penempatannya bebas pada setiap form.
  • some-> berperilaku sebagaimana -> tetapi berhenti dan mengembalikan nil pada hasil nil pertama.

Referensi#