PCL -> Clojure, Chapter 17

This article is part of a series describing a port of the samples from Practical Common Lisp (PCL) to Clojure. You will probably want to read the intro first.

This article covers Chapter 17, Object Reorientation: Classes.

Creating structs

Common Lisp defines classed with defclass. In Clojure, I can define structs with defstruct:

  (defstruct bank-account :customer-name :balance)

The bank-account struct has two basis keys: customer-name and balance. I can specify values for these keys, in the order they were declared, using struct:

  user=> (struct bank-account "John Doe" 1000)
  {:customer-name "John Doe", :balance 1000}

With struct, all the basis keys are optional:

  user=> (struct bank-account)
  {:customer-name nil, :balance nil}

If you prefer named parameters, you can use struct-map instead of struct:

  user=> (struct-map bank-account :balance 10)
  {:customer-name nil, :balance 10}

Very important: structs are still maps. I can specify additional keys that are not part of the basis:

  user=> (struct-map bank-account  :balance 10
                                       :customer-name "Jane Doe"
                                       :status :gold)
  {:customer-name "Jane Doe", :balance 10, :status :gold}

Accessing structs

The examples below assume an example-account:

  (def example-account (struct bank-account "Example Customer" 1000))

Pedants call get to access a structure value:

  user=> (get example-account :customer-name)
  "Example Customer"

But that' way too much effort. Structures are functions of their keys:

  user=> (example-account :customer-name)
  "Example Customer"

If the struct keys are symbols, I can go the other way. Symbols are functions of structs:

  user=> (:customer-name example-account)
  "Example Customer"

Other than symbols, what else can be a structure key? Ah, sweet immutability. Since Clojure data structures are immutable, any of them can function as keys.

I can use assoc and dissoc to get a new map with a key added or removed:

  user=> (assoc example-account :status :elite)
  {:customer-name "Example Customer", :balance 1000, :status :elite}

  user=> (dissoc {:a 1 :b 2} :a)
  {:b 2}

But I can't dissoc from example-account because you can never remove a basis key:

  user=> (dissoc example-account :customer-name)
  java.lang.Exception: Can't remove struct key

Defaults and validation

Since structs are also maps, default values are easy: just merge them. The example below doesn't even use a struct. (Often duck typing is good enough.)

  (def account-defaults {:balance 0})
  (defn create-account [options]
    (merge account-defaults options))

If I want to validate fields, I can just write a validation function. Here is a validation that simply requires non-false values:

  (defn validate-account [account]
    (or (every? account [:customer-name :balance])
        (throw (IllegalArgumentException. "Not a valid account"))))

Of course, if I wanted to create tons of different structs with similar validations, I could build some helpers. Macros + metadata would be one way to go.

Wrapping up

Clojure's structs fill some of the same roles as Common Lisp's classes. The exmaples above show how to create and access structs, and how to add default values and validation.

That said, Clojure's structs are not classes. They do not offer inheritance, polymorphism, etc. In Clojure, those kinds of jobs are handled by the incredibly flexible defmulti (see the previous article for details, especially the references at the end).

Notes