Let’s Write a Macro

NB: This isn’t really aa tutorial on writing Clojure macros, it’s a description of a macro I wrote and how I went about it. If your looking for an introduction to writing Clojure macros there’s an excellent one at Clojure for the Brave and True.

I’ve been working on a library for managing users and I’ve found I’ve been writing a lot of code validating parameters, it looks like this:

(if (and (string? username) (not (str/blank? username)))
(if (and (string? password) (not (str/blank? password)))
;; Do Stuff
(throw (Exception. "Invalid password")))
(throw (Exception. "Invalid username")))

This is ugly and repetitive, surely we can do better.

My first attempt was to remove the duplication by extracting some of duplicate code into a predicate:

(defn valid-str? [s]
(if (and (string? s) (not (str/blank? s)))

Thats a good start since it simplifies the code a bit, but you still writing code like:

(if (valid-str? username)
(if (valid-str? password)
;;Do Stuff
(throw (Exception.)))
(throw (Exception.)))

What I really want is a way to wrap the code that depends on the username and password so that it only executes if the values are valid or else throws the relevant exception. This sounds like a job for a macro.

A First Stab

My first attempt was to write the following:

(defmacro validate
"Checks if the parameter is valid and either executes body or throws an exception."
([s body]
`(when (valid-str? ~s)
([s body ex]
`(if (valid-str? ~s)
(throw ~ex))))

So now I could write code like:

(validate username
(validate password
;; Do Stuff

Hmmm…not that much of an improvement and my macro is limited to only validating strings, surely I can do better than that. What I really want is something that I can use like the following:

(validate predicate username
;; Do Stuff
(throw (Exception. "Invalid username")))

(validate predicate [username password]
;; Do Stuff
[(throw (Exception. "Invalid username"))
(throw (Exception. "Invalid password"))])

Now that’s a big improvement, there’s less code, it’s easier to read, the intent is clearer and we can use any thing we want as a validation function. Ok, so now I know what I want how do I get it ? Well my first thought was that this is a bit like cond.

The cond Macro

To quote the cond docstring:

Takes a set of test/expr pairs. It evaluates each test one at a time. If a test returns logical true, cond evaluates and returns the value of the corresponding expr and doesn’t evaluate any of the other tests or exprs. (cond) returns nil.

Here’s an example from Clojuredocs:

(> n 0) "negative"
(> n 0) "positive"
:else "zero")

So in my use case the code would look like:

(not (valid-str? username)) (throw (Exception. "Invalid username"))
(not (valid-str? password)) (throw (Exception. "Invalid password"))
:else ;; Do Stuff))

That’s pretty good, it’s certainly an improvement on what I’d been doing previously, but I’m not sure it’s so easy to understand the intent of the code. However given cond is a macro if we look at its source code it might give us an idea of where to start. The source of the cond macro looks like this:

(defmacro cond
"Takes a set of test/expr pairs. It evaluates each test one at a time. If a test returns logical true, cond evaluates and returns the value of the corresponding expr and doesn't evaluate any of the other tests or exprs. (cond) returns nil."
{:added "1.0"}
[& clauses]
(when clauses
(list 'if (first clauses)
(if (next clauses)
(second clauses)
(throw (IllegalArgumentException. "cond requires an even number of forms")))
(cons 'clojure.core/cond (next (next clauses))))))

Here we can see that it first checks if it has any clauses before creating an if clause and then, if it has more clauses, recursively applies itself to the remaining clauses. The code it generates is not unlike what I’d been writing originally so it definitely looks like the right approach. Whilst the cond code doesn’t give me a solution to how to write my macro it does make me think maybe I should try rewriting my it using cond.

A Second Stab

So taking that into account here’s my second attampt at the macro:

(defmacro validate
[pred s body ex]
(let [terms (interleave (map #(list not `(~pred ~%1)) s) ex)]
:else (do ~@body))))

There are a few things going on here.

First, (map #(list not `(~pred ~%1)) s) creates a sequence of cond terms by wrapping each string with the predicate term and negating the result. Then the sequence is interleaved with the sequence of handlers that need to be called if a particular string fails validation. Finally, it inserts the terms into a cond expression and adds the body of code to execute if all the strings are valid as the :else clause of the cond expression.

I can use the macro as follows:

(validate valid-str? ["abc" 123]
((println "These strings ")
(println "are all good."))
[(println "Not a valid string.")
(println "Not a valid string either.")])

Which will print Not a valid string either. to the console.

Some issues

It’s looking pretty good, but there are some issues. Firstly, the macro assumes the strings and handlers will be sequences…pass it a single value and it blows up. The easiest way to fix that is to ensure that we’re always dealing with sequences, for example:

(defmacro validate
[pred s body ex]
(let [vals (if (sequential? s) s (vector s))
handlers (if (sequential? ex) ex (vector ex))
terms (interleave (map #(list not `(~pred ~%1)) vals) handlers)]
:else (do ~@body))))

Secondly, the macro assumes the sequence of strings will be the same length as the sequence of handlers. In most circumstances they will be so this seems to be a reasonable assumption, but what about the case where you have 2 or more strings to validate but only want to provide an error handler for the first string ? Well because we use interleave to combine the strings with their handlers the macro will only interleave to the length of the shortest sequence and so in this case only the first string will be validated. One way to get around this is to manually ensure that the sequences are the same length by padding the handlers sequence with nil, but that’s messy and prone to errors. A better way is to automatically pad the sequences so they’re the same length. To do that we first need a padding function, such as:

(defn pad [vals len pad-fn]
(let [s (if (sequential? vals) vals (vector vals))]
(if (> len (count s))
(concat s (take (- len (count s)) (repeatedly pad-fn)))
(identity s))))

This function takes a sequence, the length we want to pad the sequence to and a function to generate the additional elements and returns a sequence containing the original values padded to the required length. So adding that to the macro we get:

(defmacro validate
[pred s body ex]
(let [vals (if (sequential? s) s (vector s))
handlers (pad (if (sequential? ex) ex (vector ex)) (count vals) #(identity false))
terms (interleave (map #(list not `(~pred ~%1)) vals) handlers)]
:else (do ~@body))))

The last issue with the macro is that in its current form it requires us to provide a vector of error handlers, even if the vector is empty. If we don’t provide the vector the macro blows up. So we need to be able to handle the case where no error handlers are provided.

The easiest way I can think to do that is to create a multi-arity macro so we can handle the situation where no error handlers are passed as a special case. It turns out that if we don’t have to worry about the error handlers the macro becomes much simpler since all we need to do is ensure that the predicate is valid for all elements of the input sequence and as it happens Clojure provides us a function to do just that,every?. So adding in our special handling we get:

(defmacro validate
([pred s body]
`(let [vals# (if (sequential? ~s) ~s (vector ~s))]
(when (every? ~pred vals#)
(do ~@body))))
([pred s body ex]
(let [vals (if (sequential? s) s (vector s))
handlers (pad (if (sequential? ex) ex (vector ex)) (count vals) #(identity false))
terms (interleave (map #(list not `(~pred ~%1)) vals) handlers)]
:else (do ~@body)))))


So there we have it a macro we can use for to ensure code is only executed if a sequences of values are all validated and as an aside we’ve also got a function to pad sequences so they end up the same length. There are probably some things we could do to improve the macro, but for the moment it works as required and that’s good enough for me.

This entry was posted in Clojure, General. Bookmark the permalink.