Tuesday, August 14, 2012

Another clojure macro tutorial (that no one should follow)

Disclaimer: This post shows you something you can do with macros. Not something you should do.

I like python.

You define functions

def something(x, y):
    return x + y

And you can document functions
def something(x, y):
"""Adds two things together"""
    return x + y

I also like clojure.

You can define functions
(defn something [x y]
  (+ x y))

And you can document functions
(defn something 
  "Adds to things together"
  [x y]
  (+ x y))

Documentation before the arguments? Despicable! If only there was a way of putting them in the right order.

Well, for the sake of argument let's try

Remember that in clojure code is data. A function is just a list, and we want to be able to define functions with some of the items of the list in a different order. At the moment a function definition list looks like this:

(function-name doc-string args function-body)

and we want to be able to make a function using the argument order

(function-name args doc-string function-body)

The first rule of matco club is "Don't write macros". So lets try:

First, how do we want our function (let's call it defndoc) to work? We want it to behave just like a normal function definition but with the docstring after the args.

(defndoc something [x y]
  "Adds to things together"
  (+ x y))

Now let's try to write it. We want to call our defndoc function and have that call defn with the arguments in the correct order.

(defn defndoc [fname args docs & body]
  (defn fname docs args body))

But this isn't going to work as our arguments are going to get evaluated. But this isn't what we want, looks like we will have to write a macro. This is how it looks

(defmacro defndoc [fname args docs & body]
    `(defn ~fname ~docs [~@args] ~@body))

Let's discuss the differences between this and our non-macro attempt.

First we use a syntax-quote (`). This is going to allow us to choose which bits of our list our evaluated and which are not. For example, the defn we want to be evaluated but the other parts we don't.

The next symbol is unquote (~) which tells clojure to evaluate these symbols.

The last symbol is unquote-split (~@) which tells clojure that there is a list of things here that needs to be expanded in place.

now if you do call macroexpand-1 using our defndoc macro on our function with the docstring following the arguments you will get the following

(clojure.core/defn something "Adds two things together" [x y] (+ x y))

Perfect, now we can sprinkle defndoc all over our code and have the docstring in the place we want it, but also keep clojure happy.

Now don't let me ever catch you doing this!