Week 6 Day 2 – Clojure macros and protocols

Clojure uses macros to delay execution. We are using this feature to define an unless function.

Here’s an example of an unless that does not work because it is defined with defn:

(defn unless [test body]
  (if (not test) body))

Whatever i give this function, whether the test evaluates to true or false, the body is also evaluated and placed on the stack ready to be returned. This is not what we want, so we use a macro instead.

(defmacro unless [test body]
  (list 'if (list 'not test) body))

We have to use a kind of ugly syntax, explicitly declaring a list and using the apostrophe to delay execution. Clojure uses macroexpand at the right time to evaluate the right bit of code.

We can add an else portion to the function like this:

(defmacro unless [test body other]
  (list 'if (list 'not test) body other))

Protocols

I don’t know enough about Java for this to be deeply meaningful, but i understand the principle of a java interface being something of a contract that a class has to meet.

Clojure, being a JVM language, also has a way of implementing interfaces. It calls them protocols.

I had a really hard time thinking of an example to use for the exercise. It simply said, “Write a type using defrecord that implements a protocol”. Not much to go on. But, looking ahead to day 3, i see we’re going to need bank accounts so i’m going to try that.

I’d like my protocol to look like this:

(defprotocol Account
  (total [c])
  (credit [amount])
  (debit [amount]))

An account has three functions: a total for reporting the amount in the account, and a credit and debit function for putting money in and out.

Now to implement it:

(defrecord BankAccount [opening-balance]
  Account
  (total [c] opening-balance)
  (credit [amount] (BankAccount. (+ opening-balance amount)))
  (debit [amount] (BankAccount. (- opening-balance amount))))

Notice that when i credit or debit a bank account i’m actually getting back a brand new one. Although Clojure doesn’t really enforce immutability, it’s easier to use it than not.

Now let me try to open a bank acccount with £100.

(def account (BankAccount. 100.00))
#'user/account
account
#:user.BankAccount{:opening-balance 100.0}

WOOHOO! That looks better than i expected!

Then i made a proper noob error:

(account total)
java.lang.ClassCastException: user.BankAccount cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)

Of course, reverse notation! It’s a list, so i have to call the function first:

(total account)
100.0

Brilliant. Shall we try withdrawing some money?

(debit account 20.0)
java.lang.IllegalArgumentException: No single method: debit of interface: user.Account found for function: debit of protocol: Account (NO_SOURCE_FILE:43)

I’ve got a feeling this is to do with the number of arguments. The first argument to the function is always the instance, the ‘self’. So the protocol needs to understand that:

(defprotocol Account
  (total [instance])
  (credit [instance amount])
  (debit [instance amount]))

The implementation in this case doesn’t care about the instance, so it just uses an underscore:

(defrecord BankAccount [opening-balance]
  Account
  (total [_] opening-balance)
  (credit [_ amount] (BankAccount. (+ opening-balance amount)))
  (debit [_ amount] (BankAccount. (- opening-balance amount))))

Now to try it out:

(debit account 20.0)
#:user.BankAccount{:opening-balance 80.0}

YAAAAYY!! And of course, i can call the total on that new bank account that was returned:

(total (debit account 20.0))
80.0

I can also take that bank account containing £80 and credit it with £60:

(total (credit (debit account 20.0) 60.0))
140.0

Jolly good show.

Advertisements

5 comments on “Week 6 Day 2 – Clojure macros and protocols

  1. I was not completely clear in my last response. Ken Wesson’s answer to my question came to mind, because you were talking about Java classes and how Clojure creates classes.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s