Rantai cond yang panjang menyatukan seluruh cabang perilaku di dalam satu fungsi, dan setiap perilaku baru menuntut penyuntingan pada fungsi itu. Clojure menyediakan multimethod yang mendispatch perilaku atas nilai hasil sebuah fungsi dispatch, tanpa kelas maupun pewarisan. Topik ini membahas defmulti dan defmethod, nilai dispatch default, dispatch value berupa vector, serta remove-method, dengan catatan nilai ujian sebagai contoh datanya. Setelah topik ini dibaca, penambahan perilaku baru tidak lagi berarti menyunting cabang yang sudah ada.
Dari cond ke dispatch atas nilai#
Fungsi cond menampung seluruh pilihan perilaku dalam satu badan, sehingga cabangnya bertambah setiap kali kebutuhan bertambah. Nilai yang menentukan pilihan juga tersembunyi di dalam ekspresi pembanding setiap barisnya, dan tidak ada cara untuk menambah cabang dari tempat lain. Contoh berikut memperlihatkan bentuk itu sebagai pembanding sebelum multimethod dibahas.
;; rantai cond menyatukan seluruh cabang dalam satu fungsi
(defn laporan-cond [c]
(cond
(= (:jenis c) :tugas) (str "Tugas " (:nama c) " bernilai " (:nilai c))
(= (:jenis c) :ujian) (str "Ujian " (:nama c) " bernilai " (:nilai c))
:else (str "Catatan lain dari " (:nama c))))
(println (laporan-cond {:jenis :tugas :nama "Ali" :nilai 88}))
(println (laporan-cond {:jenis :praktikum :nama "Citra" :nilai 91}))Tugas Ali bernilai 88
Catatan lain dari CitraMultimethod membalik tatanan tersebut dengan memisahkan dua hal, yaitu penentuan nilai dispatch dan perilaku per nilai dispatch. Penentuan nilai ditulis sekali pada defmulti, sedangkan perilakunya dipasang per nilai pada defmethod dari tempat mana pun, termasuk dari namespace lain. Penambahan perilaku baru dengan demikian berupa penambahan metode, bukan penyuntingan fungsi yang sudah jadi. Inilah polimorfisme atas nilai, yaitu satu nama fungsi yang berperilaku berbeda menurut nilai dispatch, tanpa perlu kelas.
Pemisahan itu juga mengubah cara nilai dispatch dibaca. Pada cond, nilai penentu tersembunyi di dalam pembanding setiap cabang, sedangkan pada multimethod ia muncul sebagai label yang terbaca langsung pada setiap defmethod. Penelusuran perilaku untuk satu nilai tertentu berubah dari membaca seluruh rantai pembanding menjadi melihat metode dengan label yang cocok.
defmulti dan defmethod#
defmulti menetapkan nama multimethod beserta fungsi dispatch-nya, yaitu fungsi yang dipanggil atas setiap argumen untuk menghasilkan nilai dispatch. defmethod memasang satu metode untuk satu nilai dispatch tertentu, dengan bentuk parameter dan badan yang sama seperti defn. Apabila nilai dispatch hasil fungsi tidak memiliki metode, pemanggilan jatuh ke metode berlabel :default.
;; defmulti menetapkan fungsi dispatch, defmethod memasang metodenya
(defmulti laporan :jenis)
(defmethod laporan :tugas [c]
(str "Tugas " (:nama c) " bernilai " (:nilai c)))
(defmethod laporan :ujian [c]
(str "Ujian " (:nama c) " bernilai " (:nilai c)))
(defmethod laporan :default [c]
(str "Catatan lain dari " (:nama c)))
(println (laporan {:jenis :tugas :nama "Ali" :nilai 88}))
(println (laporan {:jenis :ujian :nama "Budi" :nilai 76}))
(println (laporan {:jenis :praktikum :nama "Citra" :nilai 91}))Tugas Ali bernilai 88
Ujian Budi bernilai 76
Catatan lain dari CitraFungsi dispatch pada contoh di atas adalah keyword :jenis itu sendiri, karena keyword yang dipanggil sebagai fungsi mengambil nilai pasangannya dari map, dan nilai itulah yang dibandingkan dengan label setiap metode. Catatan :praktikum tidak memiliki metode, sehingga jatuh ke :default dan tetap menghasilkan jawaban yang wajar. Label :default dapat diganti dengan nilai lain melalui opsi :default pada defmulti apabila konteksnya menuntut.
Dispatch value berupa vector#
Fungsi dispatch tidak terbatas menghasilkan satu nilai sederhana. Apabila pemilihan perilaku bergantung pada beberapa dimensi sekaligus, fungsi dispatch dapat mengembalikan vector, dan metode dipasang untuk kombinasi nilai yang dimaksud. Pola ini menggantikan cabang bertingkat yang biasanya diperlukan untuk keputusan dua lapis.
;; dispatch value berupa vector: satu metode menangkap kombinasi
(defmulti status
(fn [c]
[(:jenis c) (if (>= (:nilai c) 75) :lulus :remedi)]))
(defmethod status [:ujian :lulus] [c]
(str (:nama c) " lulus ujian dengan nilai " (:nilai c)))
(defmethod status [:ujian :remedi] [c]
(str (:nama c) " mengikuti remedi ujian"))
(defmethod status :default [c]
(str "Status " (:jenis c) " bagi " (:nama c) " belum ditetapkan"))
(println (status {:jenis :ujian :nama "Budi" :nilai 76}))
(println (status {:jenis :ujian :nama "Dewi" :nilai 60}))
(println (status {:jenis :tugas :nama "Ali" :nilai 88}))Budi lulus ujian dengan nilai 76
Dewi mengikuti remedi ujian
Status :tugas bagi Ali belum ditetapkanFungsi dispatch menghasilkan vector dua elemen, yaitu jenis catatan dan status kelulusan berdasarkan batas nilai 75. Metode dipasang untuk dua kombinasi ujian, sedangkan kombinasi lain jatuh ke :default. Baris terakhir menunjukkan [:tugas :lulus] yang tidak memiliki metode, sehingga metode default menangkapnya tanpa perlu metode per kombinasi.
Bentuk vector ini juga menjadi cara menahan pertumbuhan cabang bertingkat. Pemilihan dua lapis yang biasa ditulis cond di dalam cond, atau if bersarang dua tingkat, kini tersebar sebagai metode yang berdampingan pada satu multimethod. Metode baru untuk kombinasi lain dapat ditambahkan tanpa menyentuh fungsi dispatch maupun metode yang sudah ada, dan cabang bersarang dari keputusan dua lapis itu hilang sama sekali.
remove-method dan pengelolaan metode#
Metode multimethod dapat dicabut secara terprogram. Fungsi remove-method mencabut satu metode pada satu nilai dispatch, sedangkan remove-all-methods mengosongkan seluruh metode multimethod tersebut. Kemampuan ini berguna pada REPL saat metode eksperimen perlu diganti dengan versi yang benar, tanpa perlu memuat ulang seluruh berkas sumbernya.
(defmulti laporan :jenis)
(defmethod laporan :tugas [c]
(str "Tugas " (:nama c) " bernilai " (:nilai c)))
(defmethod laporan :default [c]
(str "Catatan lain dari " (:nama c)))
(println "sebelum :" (laporan {:jenis :tugas :nama "Ali" :nilai 88}))
(remove-method laporan :tugas)
(println "sesudah :" (laporan {:jenis :tugas :nama "Ali" :nilai 88}))sebelum : Tugas Ali bernilai 88
sesudah : Catatan lain dari AliSebelum pencabutan, pemanggilan dengan :tugas diteruskan ke metode khususnya. Setelah remove-method dijalankan, nilai dispatch yang sama jatuh ke :default dan jawabannya berubah. Dengan kemampuan ini, daftar metode multimethod menjadi bagian yang dapat diubah dari lingkungan REPL, sebagaimana definisi fungsi lainnya.
Kesalahan umum#
Kesalahan pertama berupa nilai dispatch yang tidak terdaftar pada metode mana pun dan multimethod yang tidak memiliki :default. Error No method in multimethod baru muncul pada saat pemanggilan, bukan pada saat definisi, sehingga jalur data yang jarang dilalui mudah lolos dari pengujian. Perbaikannya berupa pemasangan metode :default yang menangkap seluruh nilai yang belum terdaftar.
Kesalahan kedua berkaitan dengan siklus evaluasi ulang pada REPL.
Kesalahan ketiga berupa fungsi dispatch yang berat atau berisi efek samping. Fungsi dispatch berjalan pada setiap pemanggilan, sehingga pekerjaan yang mahal di dalamnya membebani seluruh pemanggilan multimethod. Pindahkan pekerjaan yang mahal itu ke tahap persiapan data, sehingga fungsi dispatch hanya membaca nilai yang sudah tersedia.
Ringkasan#
defmultimenetapkan fungsi dispatch, sedangkandefmethodmemasang perilaku per nilai dispatch dari tempat mana pun.- Metode berlabel
:defaultmenampung seluruh nilai dispatch yang tidak terdaftar. - Fungsi dispatch dapat mengembalikan vector agar satu metode menangkap kombinasi beberapa dimensi.
remove-methodmencabut satu metode, dan keseluruhan pola ini memberikan polimorfisme atas nilai tanpa kelas.
Referensi#
- Multimethods and Hierarchies — Clojure.org — Referensi resmi multimethod, termasuk nilai default, dispatch berlapis, dan hierarki.
- defmulti — ClojureDocs
— Dokumentasi
defmultibeserta opsi:defaultpada pemanggilannya. - defmethod — ClojureDocs
— Dokumentasi
defmethoduntuk memasang metode per nilai dispatch.