Map literal melayani sebagian besar kebutuhan data pada Clojure, tetapi sejumlah situasi menuntut tipe host yang sesungguhnya: data domain yang lewat banyak lapisan program, data yang diteruskan ke pustaka Java, atau perilaku yang harus mengimplementasikan interface tertentu. Topik ini membahas dua bentuk definisi tipe yang disediakan Clojure. defrecord menghasilkan tipe data yang berperilaku seperti map, sedangkan deftype menghasilkan tipe rendah dengan field final dan implementasi eksplisit. Bagian akhir membahas kriteria pemilihan keduanya.
defrecord: data dengan nama dan tipe#
defrecord mendefinisikan tipe host baru beserta field-fieldnya, dan Clojure otomatis membuat dua fungsi konstruktor. Fungsi ->Nama menerima nilai field secara posisional, sedangkan map->Nama menerima satu map dan dibahas pada bagian berikut. Record yang dihasilkan dapat dibaca lewat keyword karena record mengimplementasikan interface pencarian yang sama dengan map.
(defrecord Titik [x y])
(def t (->Titik 3 4))
(println (:x t) (:y t))
(prn t)
(println (.x t))3 4
#user.Titik{:x 3, :y 4}
3Ekspresi (:x t) berjalan karena keyword adalah fungsi yang mencari dirinya pada map maupun record. Fungsi prn mencetak record dengan tag pembaca #user.Titik, yaitu nama tipe disertai namespace tempat tipe itu didefinisikan. Akses .x juga sah karena field record dibuat publik dan final oleh compiler, berbeda dengan deftype yang membuat fieldnya privat. Kedua cara akses menghasilkan nilai yang sama, tetapi gaya Clojure umumnya memilih keyword karena akses itu tetap berlaku ketika record ditukar dengan map biasa.
Operasi map pada record#
Record berperilaku seperti map, sehingga operasi map yang tidak mengubah koleksi langsung berlaku. assoc menghasilkan record baru, into mengubah record menjadi map biasa, count menghitung pasangannya, dan kesetaraan dinilai berdasarkan nilai.
(defrecord Titik [x y])
(def t (->Titik 3 4))
(def t2 (assoc t :y 10))
(prn t2)
(println (instance? Titik t2))
(prn t)
(prn (into {} t))
(println (count t))
(println (= (->Titik 3 4) (->Titik 3 4)))
(def t3 (assoc t :z 9))
(prn t3)
(println (instance? Titik t3))
(println (count t3))#user.Titik{:x 3, :y 10}
true
#user.Titik{:x 3, :y 4}
{:x 3, :y 4}
2
true
#user.Titik{:x 3, :y 4, :z 9}
true
3Hasil assoc adalah record baru dengan y bernilai 10, sedangkan t lama tetap {:x 3, :y 4} karena koleksi Clojure immutable. Pemanggilan instance? mengembalikan true, menegaskan bahwa hasil assoc tetap objek Titik dan bukan map biasa. Fungsi into menghasilkan map biasa tanpa tipe, dan hasil cetakannya tidak lagi memuat tag pembaca. Kesetaraan dinilai dari isi, sehingga dua record dengan field sama dianggap =. Kunci baru di luar field juga dapat ditambahkan lewat assoc, dan record hasilnya tetap bertipe Titik.
Konstruksi dari map#
Fungsi map->Nama mengonstruksi record dari sebuah map, kunci demi kunci. Bentuk ini berguna ketika data datang sudah berbentuk map, misalnya hasil pembacaan berkas atau baris basis data, dan masih perlu diberi tipe agar seluruh lapisan program mengetahui bentuknya.
(defrecord Titik [x y])
(prn (map->Titik {:x 1 :y 2}))
(prn (map->Titik {:x 1 :y 2 :catatan "pusat"}))
(println (:catatan (map->Titik {:x 1 :y 2 :catatan "pusat"})))#user.Titik{:x 1, :y 2}
#user.Titik{:x 1, :y 2, :catatan "pusat"}
pusatKunci :catatan bukan field Titik, tetapi tetap tersimpan pada extmap dan terbaca lewat keyword. Apabila map sumber kehilangan salah satu field, field yang hilang diberi nilai nil, sehingga konstruksi tidak pernah melempar error. Kontranya adalah ->Titik yang posisional: jumlah argumennya wajib sama dengan jumlah field, dan kekurangan argumen menyebabkan error saat konstruksi. Pilihan antara keduanya mengikuti sumber data, yaitu map->Nama untuk data berbentuk map dan ->Nama untuk nilai yang sudah diketahui posisinya.
deftype: tipe rendah#
deftype mendefinisikan tipe pada tingkat yang sama dengan compiler Java: fieldnya privat dan final, tidak ada perilaku map yang dibawa, dan setiap metode harus dideklarasikan secara eksplisit. Tipe ini ditujukan untuk kebutuhan mekanis, misalnya mengimplementasikan interface Java tertentu dengan kontrol penuh atas field. Konstruktornya berbentuk titik seperti (Persegi. 6 2), dan tidak ada fungsi pabrik (factory function) yang dibuat otomatis.
(deftype Persegi [lebar tinggi]
Object
(toString [this]
(str "Persegi " lebar "x" tinggi)))
(def p (Persegi. 6 2))
(println (str p))
(println (instance? Persegi p))
(println (:lebar p))
(println (= (Persegi. 6 2) (Persegi. 6 2)))
(println (= p p))Persegi 6x2
true
nil
false
trueDeklarasi Object diikuti toString berarti tipe ini menimpa (override) metode toString milik Object, dan badan metode boleh membaca field lewat namanya secara langsung. Tiga baris terakhir memperlihatkan akibat sifat tipe rendah ini. Akses (:lebar p) mengembalikan nil karena field deftype privat, sehingga pencarian keyword tidak menemukan apa pun. Kesetaraan dinilai berdasarkan identitas, sehingga dua Persegi dengan isi sama tidak dianggap =, sedangkan satu objek yang dibandingkan dengan dirinya tetap sama. Apabila perilaku map dan kesetaraan nilai diinginkan, jawabannya adalah defrecord, bukan tambahan implementasi manual.
Kapan record, kapan deftype#
| Situasi | Pilihan | Alasan |
|---|---|---|
| Data domain yang mengalir antarfungsi | defrecord | akses keyword, kesetaraan nilai, fungsi map berlaku |
| Data yang diteruskan ke API Java bertipe | defrecord atau deftype | keduanya menghasilkan class JVM nyata |
| Implementasi interface Java dengan perilaku khusus | deftype | field privat dan implementasi eksplisit |
| Nilai sementara di dalam fungsi | map literal | tanpa definisi tipe, langsung terbaca |
Record adalah pilihan bawaan untuk data karena memberikan tipe bernama tanpa memindahkan perilaku map. Sebaliknya, deftype disisakan untuk kebutuhan yang lebih rendah, yaitu implementasi interface host yang tidak nyaman diekspresikan lewat protocol, kontrol penuh atas field, atau kebutuhan performa tertentu. Selain itu, deftype menjadi satu-satunya bentuk yang mengizinkan field mutable melalui opsi :volatile-mutable dan :unsynchronized-mutable, kebutuhan yang lazim pada pustaka tingkat rendah. Apabila kedua pilihan itu tidak dibutuhkan, map literal tetap menjadi bentuk data yang paling ringan karena tidak ada definisi tipe yang harus dijaga.
Kesalahan umum#
- Mengakses field deftype lewat keyword, misalnya
(:lebar p). Hasilnya selalu nil karena field deftype privat. Tulis metode aksesor di dalam deftype, atau gunakan defrecord. - Mengharapkan dua objek deftype dengan isi sama dianggap
=. Kesetaraan deftype mengikuti identitas objek. Implementasikan equals secara eksplisit, atau gunakan defrecord. - Menganggap assoc mengubah record lama. Hasilnya record baru, sedangkan record lama tetap utuh. Ikat hasilnya ke nama baru bila versi terbaru ingin digunakan lebih lanjut.
- Mendefinisikan ulang defrecord atau deftype dengan bentuk berbeda pada sesi REPL yang sama. Definisi tipe lama masih terikat di banyak tempat dan sering memunculkan error yang membingungkan. Gunakan namespace baru atau mulai ulang REPL saat eksperimen mengubah bentuk tipe.
Ringkasan#
- defrecord menghasilkan tipe host dengan field bernama yang sekaligus berperilaku seperti map, lengkap dengan konstruktor
->Namadanmap->Nama. - Operasi map atas record menghasilkan record baru, kunci tambahan tersimpan pada extmap, dan
instance?menegaskan bahwa tipe host tetap terjaga. - deftype adalah tipe rendah dengan field privat final, tanpa perilaku map, dengan kesetaraan berbasis identitas, dan dengan implementasi metode yang eksplisit.
- Pilih record untuk data dan deftype untuk mekanisme host, sedangkan map literal tetap digunakan untuk nilai sementara.
Referensi#
- Data Types — Clojure — Referensi resmi deftype, defrecord, perbedaannya, dan opsi field mutable.
- defrecord — ClojureDocs — Contoh defrecord termasuk konstruktor ->Nama dan map->Nama.
- deftype — ClojureDocs — Contoh deftype beserta implementasi metode dan interface.