Protocol adalah mekanisme dispatch polimorfik milik Clojure, yaitu pemilihan implementasi berdasarkan tipe argumen pertama. Satu protocol mendeklarasikan sekelompok fungsi, dan tipe apa pun dapat menyediakan implementasinya, termasuk tipe yang tidak ditulis oleh orang yang sama. Topik ini mendefinisikan protocol dengan defprotocol, mengimplementasikannya pada defrecord, memperluasnya ke tipe bawaan lewat extend-protocol, dan menutup dengan perbandingan terhadap multi-method. Inti seluruh pembahasannya adalah perluasan perilaku tanpa inheritance.
Protocol sebagai kontrak polimorfik#
defprotocol mendeklarasikan nama protocol beserta fungsi-fungsinya, lengkap dengan daftar parameter tiap fungsi. Deklarasi ini menghasilkan fungsi biasa yang dapat dipanggil dari mana saja, dan pemilihan implementasinya terjadi berdasarkan tipe argumen pertama. Implementasi dapat menyatu pada defrecord atau deftype lewat nama protocol yang diikuti pasangan metode.
(defprotocol Deskripsi
(jelaskan [x]))
(defrecord Siswa [nama kelas]
Deskripsi
(jelaskan [this]
(str nama " duduk di kelas " kelas)))
(println (jelaskan (->Siswa "Ali" "8B")))
(println (jelaskan (->Siswa "Dewi" "7A")))Ali duduk di kelas 8B
Dewi duduk di kelas 7AProtocol Deskripsi hanya memuat satu fungsi bernama jelaskan dengan satu parameter. Record Siswa mendaftarkan implementasinya tepat di badan definisi, dan parameter pertama yang lazim diberi nama this menunjuk objek itu sendiri. Di dalam badan implementasi, field record bernama nama dan kelas dapat dirujuk langsung seperti variabel lokal. Fungsi jelaskan yang dihasilkan defprotocol adalah fungsi biasa, sehingga dapat diteruskan ke fungsi map, disimpan pada var, dan dipanggil atas objek apa pun yang sudah menyediakan implementasinya.
extend-protocol atas tipe bawaan#
Protocol tidak terikat pada tipe yang didefinisikan sendiri. extend-protocol menempelkan implementasi ke tipe yang sudah ada, termasuk kelas JDK dan nil, tanpa menyentuh definisi tipe tersebut. Satu pemanggilan extend-protocol dapat menjangkau beberapa tipe sekaligus, dengan satu blok implementasi per tipe. Kebalikannya adalah extend-type, yaitu satu tipe diperluas dengan beberapa protocol sekaligus, dan keduanya menggunakan bentuk implementasi yang sama sehingga perpindahan di antara keduanya hanya masalah sudut pandang.
(defprotocol Ukuran
(ukuran [x]))
(extend-protocol Ukuran
String
(ukuran [s] (.length s))
Long
(ukuran [n] n)
nil
(ukuran [x] 0))
(println (ukuran "kalkulator"))
(println (ukuran 12))
(println (ukuran nil))10
12
0String diperluas dengan delegasi ke (.length s), Long dikembalikan apa adanya karena pada contoh ini bilangan itu sendiri sudah menjadi ukurannya, dan nil diberi ukuran nol. Target nil ditulis apa adanya dan sah sebagai target dispatch, kebiasaan yang berguna untuk menangani data yang belum ada. Dispatch mengikuti hierarki kelas Java, sehingga perluasan atas Number akan menjangkau Long, Integer, dan Double sekaligus. Pemanggilan atas tipe yang belum diperluas melempar error, dan penanganannya dibahas pada bagian Kesalahan umum.
Perluasan dari dua arah tanpa inheritance#
Kekuatan protocol terlihat ketika kedua arah perluasan digabungkan. Tipe yang sudah ada memperoleh perilaku baru, dan tipe baru lahir dengan langsung memenuhi kontrak yang sudah ada. Tidak ada satu pun definisi tipe yang harus diubah, dan tidak ada hubungan inheritance yang diperlukan di antara keduanya.
(defprotocol Ukuran
(ukuran [x]))
(extend-protocol Ukuran
String
(ukuran [s] (.length s)))
(defrecord Berkas [isi]
Ukuran
(ukuran [this]
(count isi)))
(println (ukuran "kalkulator"))
(println (ukuran (->Berkas "simpan")))10
6Arah pertama, String memperoleh fungsi ukuran dari luar tanpa definisi String disentuh sedikit pun. Arah kedua, record Berkas yang didefinisikan belakangan mendaftarkan implementasinya pada protocol yang sudah ada sebelumnya. Pada Java, menambahkan metode ke String tidak mungkin tanpa mengubah kelasnya, dan kelas baru harus ditulis dengan pengetahuan akan seluruh interface yang ingin dipenuhinya. Protocol membalik kedua batasan itu, sehingga perilaku dan tipe dapat dikembangkan secara terpisah. Pola inilah yang sering disebut sebagai jawaban Clojure atas expression problem, yaitu kesulitan menambah perilaku baru atas tipe baru maupun tipe lama tanpa mengubah kode yang sudah stabil.
Dispatch atas tipe berbeda dari dispatch atas nilai#
Clojure juga menyediakan multi-method, mekanisme dispatch yang memilih implementasi berdasarkan nilai kembalian fungsi dispatch, bukan berdasarkan tipe argumen. Contoh berikut menghitung luas dari map biasa yang nilai :bentuk-nya menentukan rumus.
(defmulti luas :bentuk)
(defmethod luas :persegi-panjang [s]
(* (:panjang s) (:lebar s)))
(defmethod luas :lingkaran [s]
(* Math/PI (:jari-jari s) (:jari-jari s)))
(println (luas {:bentuk :persegi-panjang :panjang 5 :lebar 2}))
(println (luas {:bentuk :lingkaran :jari-jari 7}))10
153.93804002589985multi-method luas didispatch atas nilai :bentuk dari map biasa, sehingga tidak ada tipe yang diperlukan sama sekali. Perbedaannya dengan protocol terletak pada dasar pemilihan: protocol menempel pada tipe argumen pertama, sedangkan multi-method menempel pada nilai apa pun yang dikembalikan fungsi dispatch-nya. Tabel berikut merangkum perbedaannya.
| Aspek | protocol | multi-method |
|---|---|---|
| Dasar pemilihan | tipe argumen pertama | nilai kembalian fungsi dispatch |
| Jumlah fungsi | beberapa fungsi dalam satu protocol | satu fungsi per definisi |
| Nilai bawaan | tidak ada | tersedia lewat :default |
| Data yang dilayani | objek bertipe, termasuk record dan kelas Java | data apa pun, termasuk map biasa |
Kaidah pemilihannya cukup lugas. Gunakan protocol ketika perilaku mengikat pada tipe, misalnya ukuran atas String dan record, terlebih karena protocol mampu memuat beberapa fungsi yang berkaitan dalam satu kontrak. Gunakan multi-method ketika pemilihan bergantung pada nilai data, misalnya bentuk geometri pada map, atau ketika nilai bawaan :default diperlukan. Keduanya saling melengkapi, bukan saling menggantikan, dan keduanya sama-sama bekerja tanpa inheritance.
Kesalahan umum#
- Memanggil fungsi protocol atas tipe yang belum diperluas, misalnya
(ukuran [1 2 3]). Clojure melempar IllegalArgumentException dengan pesanNo implementation of method: :ukuran of protocol. Perluas tipe tersebut atau periksa tipe argumen sebelum memanggil. - Menulis Nil sebagai target nil pada extend-protocol. Target yang benar ditulis nil dalam huruf kecil, karena nil adalah nilai, bukan nama kelas.
- Mengubah jumlah parameter pada implementasi. Arity implementasi wajib sama dengan deklarasi defprotocol, dan ketidaksesuaian menyebabkan error saat fungsi protocol dipanggil.
- Mengira extend-protocol berlaku hanya untuk kode pada blok yang sama. Perluasan bersifat global atas tipe, sehingga implementasi baru menggantikan implementasi lama untuk tipe yang sama di seluruh program.
Ringkasan#
- defprotocol mendeklarasikan sekelompok fungsi yang didispatch berdasarkan tipe argumen pertama.
- Implementasi dapat menyatu pada defrecord dan deftype, atau ditempelkan dari luar dengan extend-protocol, termasuk atas String, Long, dan nil.
- Perluasan bekerja dua arah tanpa inheritance, yaitu tipe bawaan memperoleh perilaku baru dan tipe baru memenuhi kontrak lama.
- protocol memilih implementasi atas tipe, sedangkan multi-method atas nilai data, dan keduanya saling melengkapi untuk kebutuhan yang berbeda.
Referensi#
- Protocols — Clojure — Referensi resmi protocol, extend-protocol, extend-type, dan dispatch hierarki tipe.
- defprotocol — ClojureDocs — Contoh pendefinisian protocol dan implementasinya pada tipe.
- extend-protocol — ClojureDocs — Contoh perluasan satu protocol ke beberapa tipe sekaligus.