deftype dan defrecord

Membuat tipe host konkret dengan deftype dan record bernilai mirip map dengan defrecord.

Daftar isi

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.

clojure Mendefinisikan dan mengonstruksi record Titik
(defrecord Titik [x y])

(def t (->Titik 3 4))
(println (:x t) (:y t))
(prn t)
(println (.x t))
text Keluaran
3 4
#user.Titik{:x 3, :y 4}
3

Ekspresi (: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.

clojure Record diolah seperti map
(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))
text Keluaran
#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
3

Hasil 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.

clojure Konstruksi record dari map
(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"})))
text Keluaran
#user.Titik{:x 1, :y 2}
#user.Titik{:x 1, :y 2, :catatan "pusat"}
pusat

Kunci :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.

clojure deftype dengan implementasi metode sendiri
(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))
text Keluaran
Persegi 6x2
true
nil
false
true

Deklarasi 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#

SituasiPilihanAlasan
Data domain yang mengalir antarfungsidefrecordakses keyword, kesetaraan nilai, fungsi map berlaku
Data yang diteruskan ke API Java bertipedefrecord atau deftypekeduanya menghasilkan class JVM nyata
Implementasi interface Java dengan perilaku khususdeftypefield privat dan implementasi eksplisit
Nilai sementara di dalam fungsimap literaltanpa 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 ->Nama dan map->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#