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.

(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-nameversion&args]`(let [args#~(unquote-project(argument-list->argument-mapargs))root#~(.getParent(io/file*file*))](def ~'project(makeargs#'~project-name~versionroot#))))

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:

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 (:rootp)"/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:

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.