Records and Types
Prerequisites: Protocols
Most Clojure data is plain maps. When you need a named type — for protocol dispatch, Java interop, or performance — use a record.
(defrecord Point [x y])
(def p (->Point 1 2)) ; positional constructor
(def q (map->Point {:x 3 :y 4})) ; map constructor
(println p)
(println (:x p) (:y q))
(println (assoc p :x 10)) ; still a Point
(println (assoc p :label "origin-ish")) ; extra keys are allowed
(println (= p (->Point 1 2)))
(println (map? p) (instance? Point p))
(println (dissoc p :x)) ; removing a field: plain map
#user.Point{:x 1, :y 2}
1 4
#user.Point{:x 10, :y 2}
#user.Point{:x 1, :y 2, :label origin-ish}
true
true true
{:y 2}
defrecordgenerates a Java class with the fields, plus->Pointandmap->Pointconstructor functions.- A record is a map: keyword access,
assoc,update, destructuring, and every map function work. - Records have value equality, and field access is faster than map lookup.
- Removing a declared field turns it back into a plain map.
deftype
(deftype Counter [^:volatile-mutable n]
clojure.lang.IDeref
(deref [_] n)
Object
(toString [_] (str "Counter(" n ")")))
(def c (Counter. 5))
(println @c (str c))
(println (map? c))
5 Counter(5)
false
deftype creates a bare class with no map behavior — for low-level
building blocks like custom collections or mutable counters. Application
code rarely needs it.
Choosing
| Use | For |
|---|---|
| Plain map | Almost all domain data |
defrecord |
Protocol implementations; hot paths; Java interop |
deftype |
New data structures or low-level types |
reify |
A one-off object implementing an interface or protocol |
Start with maps; switch to records when you have a concrete reason. Code written against maps keeps working, because records are maps.