In clojure
, records, types and protocols are parts of the fundamental building blocks of the language.
We have unveiled defprotocol
’s secret in a previous post. Now it’s time to explore defrecord
.
This article has been - inspired by this great talk by Michał Marczyk: defrecord/deftype in Clojure and ClojureScript.
Michał knows this topic very deeply as he contributed to the implementation of types, records and
protocols in clojurescript
in 2012.
He’s also the #4 contributor on clojurescript
.
We were very honoured when Michał encouraged us to write a blog post on the topic using KLIPSE.
Introduction
What is a record?
In computer science, a record (also called struct or compound data) is a basic data structure. A record is a collection of fields, possibly of different data types, typically in fixed number and sequence. Wikipedia
In clojure
, defrecord
, deftype
and defprotocol
were introduced in version 1.2
In clojurescript
the persistent data structures (maps, vectors …) are based on types, records and protocols.
Records: creation and identity
There are 3 equivalent ways to create a record: a constructor and two factory functions: (A.)
, (->A)
and (map->A)
.
(defrecord A [x y])
(def a (A. 1 2))
(def aa (map->A {:x 1 :y 2}))
(def aaa (->A 1 2))
[a aa aaa]
You can retrieve the basis keys of the record with getBasis
that returns a vector of basis keys as symbols.
(.getBasis A)
Records of the same type and same values are equal
(= a aa aaa)
And have the same hash code.
(map hash [a aa aaa])
Records of different types are never equal (even if they have the same values).
(defrecord B [x y])
(def b (B. 1 2))
(= a b)
In clojurescript
, the hash
function doesn’t take the record type into account.
(map hash [a b])
Maps
At first glance, records behave like maps: keyword access, get
, count
, keys
and iteration work as expected.
[(:x a) (get a :z "n/a")]
(keys a)
(count a)
(map (fn [[k v]] [k (inc v)]) a)
But records are not real maps. Michał called them Wacky maps.
Wacky maps
Here are the differences between records and maps:
- Unlike a regular map, a record is not callable as a function.
- When you
assoc
a record you get another record. - When you
dissoc
basis keys in a record you get a map instead of a record. - In
clojure
boxing happens on records when you use type hints; but not inclojurescript
. (Boxing can be avoided with type hints, if you use field access syntax and you’re in a primitive-friendly context e.g. an arithmetic expression.)
assoc
works as expected:
(assoc a :z "zzz")
But dissoc
is surprising:
(dissoc a :x)
Behind the scenes : defrecord’s transpiled javascript code
If you are curious to see how the magic occurs in clojurescript
, you will find it very interesting to observe and meditate around the transpiled javascript
code of defrecord
:
(defrecord A [x y])
If you want to discover stuff you didn’t know about deftype
, go to The power and danger of deftype…
Clojure & Clojurescript rock!