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.

Michal Marczyk

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 in clojurescript. (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!