muhuk's blog

Nature, to Be Commanded, Must Be Obeyed

July 01, 2014

Clojure Macro Spotted In Wild: defproject of Leiningen

“Programs must be written for people to read, and only incidentally for machines to execute.”

—Harold Abelson

Reading code is a great way to improve programming skill. It is not just a progressive practice though. I have observed that those who are not in the habit of reading code lose their edge eventually. Perhaps they didn’t have an edge to begin with and their lack of experience was overrated when they are evaluated. Regardless, I stand by the practice of reading code.

This is not going to be a series of posts with continuity or any plan. I will share my observations of interesting or advanced or cool Clojure macros as I find them. And they will be from real world code, so no theoretical, made up stuff.

What better project to start than Leiningen? Here is the code we will be studying:

(defmacro defproject
  "The project.clj file must either def a project map or call this macro.
  See `lein help sample` to see what arguments it accepts."
  [project-name version & args]
  `(let [args# ~(unquote-project (argument-list->argument-map args))
         root# ~(.getParent (io/file *file*))]
     (def ~'project
       (make args# '~project-name ~version root#))))

I don’t know if you have noticed it before but typically every project.clj file contains a call to this macro. Let’s chop it up a little bit so we can see the form it’s going to build:

(defmacro defproject
  `(let [..local-bindings...]
     (def ~'project
       (make ..arguments-for-make..))))

It creates a def form, which in turn creates a var when evaluated. Often macros that are called in the top level of a namespace follow a similar pattern. Nothing surprising here but two questions come to mind: why is this a macro instead of a function? And why aren’t the local bindings interned into the make call, avoiding the let?

As far as I can understand the answer to the first question is; to delay execution of args, the variable length argument to defproject. It seems unquote-project is preserving forms prefixed with ~ (unqote) and quoting everything else. Let’s take a look at sample.project.clj:

;; Paths to include on the classpath from each project in the
;; checkouts/ directory. (See the FAQ in the Readme for more details
;; about checkout dependencies.) Set this to be a vector of
;; functions that take the target project as argument. Defaults to
;; [:source-paths :compile-path :resource-paths], but you could use
;; the following to share code from the test suite:
:checkout-deps-shares [:source-paths :test-paths
                      ~(fn [p] (str (:root p) "/lib/dev/*"))]
:filespecs [...
            ;; Programmatically-generated content can use :bytes.
            {:type :bytes :path "project.clj"
            ;; Strings or byte arrays are accepted.
            :bytes ~(slurp "project.clj")}

This allows you to run arbitrary code. It is necessary, because if unqote-project quotes the fn, it will never be evaluated:

user=> (defmacro foo [v] `(def ~v `(fn [i#] i#)))

user=> (foo a)

user=> a
(clojure.core/fn [user/i__720__auto__] user/i__720__auto__)

user=> (a 2)

ClassCastException clojure.lang.Cons cannot be cast to clojure.lang.IFn  user/eval704 (NO_SOURCE_FILE:1)

user=> (defmacro bar [v] `(def ~v (fn [i#] i#)))

user=> (bar b)

user=> b
#<user$b user$b@73d9f898>

user=> (b 3)

Another thing to note is unquote & quote combination; ~'project. It serves the simple purpose of creating an unqualified (no namespace) symbol [1]. To see 'x, ~x and ~'x in action:

user=> (defmacro foo [x] `(do (def ~x 'x) (def ~'x nil)))

user=> (macroexpand '(foo a))
(do (def a (quote user/x)) (def x nil))

Finally gensym‘s are used to prevent variable capture:

user=> (def x 5)

user=> (defmacro foo [] `(let [x# 7] (list x ~x x#)))

user=> (foo)
(5 5 7)

user=> (macroexpand '(foo))
(let* [x__717__auto__ 7] (clojure.core/list user/x 5 x__717__auto__))

I am not going to go into the specifics of unqote-project. It returns a quoted form as far as I can tell. This is another common Clojure idiom; keeping the actual macro as lean as possible and delegating most of the functionality to plain old functions that return quoted forms. What makes LISP so powerful can be summed up as code is data and data is code. Quoted forms are data. Data that can be evaluated. Building s-expressions with functions makes it easier to reason with and also easier to test. Then you can take these forms and glue them together in your macro.

Let me know if you know of any interesting or advanced or cool macros.

[1]See What is the purpose of ~’ or ‘~ in Clojure?

If you have any questions, suggestions or corrections feel free to drop me a line.