This post continues our Clojure introduction (Part 1, Part 2, Part 3). This time we‘re covering compound data.

In Part 2 of the Clojure introduction, we looked at built-in data structures. Today we‘ll cover the definition of new compound data types.

As an example, let‘s go to a computer store and put together a custom computer from individual components. To describe this configuration (for example, for an online shop), we‘ll design a representation as data. This process starts with a data definition:

; A computer consists of:
; - Processor
; - RAM capacity in Gbyte
; - Hard drive capacity in Gbyte

The data definition clearly states that a computer consists of several parts, so we need compound data. In Clojure, records are responsible for compound data, and we can define a record type for compound data using the defrecord form:

(defrecord Computer
    [processor ram hard-drive])

This definition creates a Java class named Computer, with fields processor, ram, and hard-drive. (Java programmers would call Computer a POJO class). Since Java classes typically start with a capital letter, we‘ll adopt this convention for record types.) This allows us to use the built-in Clojure new construct to create Computer objects:

(def gamer (new Computer "Cell" 4 1000)) ; Cell, 4 Gbyte RAM, 1000 Gbyte hard drive
(def workstation (new Computer "Xeon" 2 500)) ; Xeon, 2 Gbyte RAM, 500 Gbyte hard drive

We can abbreviate new Computer as Computer. (note the period):

(def workstation (Computer. "Xeon" 2 500))

However, it should be noted that Computer. is not a function, but a macro. If we try to use it as a standalone object, the following happens:

records> Computer.
CompilerException java.lang.ClassNotFoundException: Computer.

Fortunately, Clojure also provides a constructor function called ->Computer:

(def workstation (->Computer "Xeon" 2 500))

We can extract the individual parts of a record as follows:

records> (:processor gamer)
"Cell"
records> (:ram gamer)
4
records> (:hard-drive gamer)
1000

The keywords with the field names thus function as selectors - this is because in Clojure, every record also works as a map. (See our introduction to data types.)

That‘s actually enough to work with records in Clojure. However, compound data often also appears as cases in mixed data, for which we still need a predicate for a record type, i.e., a way to distinguish objects of the record definition from other objects.

To illustrate, we‘ll write a program to feed two types of animals: armadillos and parrots.

Here‘s a data definition for armadillos, a matching record definition, and an example armadillo:

; An armadillo has the following properties:
; - Weight (in g)
; - alive or dead
(defrecord Dillo
    [weight alive?])

(def d1 (->Dillo 55000 true)) ; 55 kg, alive

… and here‘s the same thing again for the parrot:

; A parrot has the following properties:
; - Weight in grams
; - Sentence it says
(defrecord Parrot
  [weight sentence])

(def p1 (->Parrot 10000 "The gardener did it.")) ; 10kg, Miss Marple

Feeding an armadillo increases its weight, at least if it‘s still alive:

(defn feed-dillo
  "Feed armadillo."
  [d]
  (if (:alive? d)
    (->Dillo (+ (:weight d) 500) true)
    d))

For a parrot, it always works:

(defn feed-parrot
  "Feed parrot."
  [p]
  (->Parrot (+ (:weight p) 50)
            (:sentence p)))

Now let‘s write a function that handles both armadillos and parrots. The data definition for this looks like:

; An animal is one of the following:
; - an armadillo
; - a parrot

To write a feed-animal function, we need to make a conditional branch depending on whether an animal is an armadillo or a parrot. This works with instance?. (instance? C x) tests whether x is an instance of class C, particularly whether x belongs to record type C:

(defn feed-animal
  "Feed animal."
  [a]
  (cond
   (instance? Dillo a) (feed-dillo a)
   (instance? Parrot a) (feed-parrot a)))

With that, we have all the components we need for working with new types for compound data: constructor, selectors, and predicate. Enough for today!