On Clojure

February 23, 2010

Generating deftype forms in macros

Filed under: General — khinsen @ 2:36 pm

One of the common uses of macros in Clojure, as in other Lisp dialects, is to abstract away boilerplate code. Instead of writing very similar lengthy forms several times, one defines a macro that specializes a template for each particular use, and then uses that macro a few times. The template is usually written using syntax-quote: the template form is preceded by a backquote, and inside the template a tilde marks expressions that are replaced by their values.

Syntax-quote has one more effect: it resolves all symbols in the current namespace (the one in which the macro is defined, not the one where it is used) and replaces the unqualified symbol by its namespace-qualified equivalent. For most symbols in most forms, this is the right thing to do in order to make the macro work in any namespace, as well as to avoid unwanted variable capture. More specifically, it is the right thing to do for symbols that are defined by the macro, and for symbols that will ultimately be evaluated (names referring to vars, in particular function names). It is not the right thing to do for symbols bound locally inside the form (function parameter names, symbols bound in a let form). And it is also not the right thing to do for symbols that just stand for themselves and are used in some special way by the form that the macro expands to.

The latter situation is particularly frequent in macros that generate deftype forms. Consider for example the following deftype form, which is a simplified version of the type definition used in my multiarray design study:

(deftype multiarray

  [descriptor
   data-array]
  :as this

  Object
    (equals [o] ...)
    (hashCode [] ...)

   clojure.lang.Counted
     (count [] ...)

   clojure.lang.Indexed
     (nth [i] ..)

   clojure.lang.Sequential

   clojure.lang.Seqable
     (seq [] ...))

Of all the symbols shown in the above example, the only one for which namespace-resolution is appropriate is multiarray, the name of the type being defined. All the other symbols name fields of the type, Java interfaces, or methods. They must remain unqualified. In real-life deftypes, there are of course symbols that could or should be namespace-qualified, in particular most of the symbols used inside the method definitions, which are just like function definitions. However, method definitions are often short, and rarely subject to variable capture, meaning that not namespace-resolving those symbols is rarely a problem.

In a syntax-quote template, there are two ways to deal with symbols for which the default (namespace-resolution) is not appropriate:

  • Prefixing with ~' (tilde + quote). This is a special case of an expression inside a template, whose value is the quoted symbol. A tilde-quoted symbol is taken over into the instantiated template without namespace-resolution.
  • Postfixing with # (hash sign). Such symbols are replaced with system-generated symbols that are guaranteed to be different from any other symbol in existence. This is another technique to avoid variable capture.

For generating a deftype form from a syntax-quote template, the only solution is thus to prefix all the symbols shown in the example above with tilde-quote. I tried: it works, but it’s a mess. It’s not very readable, and the inevitable mistakes lead to unpleasant error messages.

Well, this is Lisp, and in Lisp you are always free to make your own tools if you are not happy with the ones provided by the system. What I want here is a template expansion system that doesn’t do namespace resolution on symbols. However, I didn’t need a full-blown equivalent to syntax-quote templates either, given that I would use those deftype templates only for one application. So I came up with the following definitions, which for me are the right compromise between simplicity and useability:

(defn instantiate-template
  [substitution-map form]
  (clojure.walk/prewalk
   (fn [x] (if (and (sequential? x) (= (first x) 'clojure.core/unquote))
	     (substitution-map (second x))
	     x))
   form))

(defmacro template
  [substitutions form]
  (let [substitution-map (into {} (map (fn [[a b]]
					 [(list 'quote a) b])
				       (partition 2 substitutions)))]
    `(instantiate-template ~substitution-map (quote ~form))))

Compared to syntax-quote, this has two restrictions: it has no splicing, and it admits only symbols after a tilde, not arbitrary expressions. The template macro takes a let-like vector as its first argument. This vector contains the symbol-value pairs for substitution inside the template. The second argument is the template form, which presumably contains tilde-prefixed symbols for substitution. Note that the Clojure reader translates ~x to <code (clojure.core/unquote x), which is what the above code searches for.

Here is an example for using such templates:

(defmacro foo [typename fieldname]
  (template [type  typename
	     field fieldname]
    (deftype ~type
      [~field])))

(foo bar boo)

(bar 42)

This prints #:bar{:boo 2}, illustrating that the macros does what it is expected to do. Of course this is not the perfect example for the utility of my little template instantiation system, as it could just as well be written using syntax-quote!

About these ads

1 Comment »

  1. The thing I don’t like about syntax-quote (versus quote) is that it makes the expanded code verbose and unreadable. Then it’s hard to debug the macro with macroexpand-1.

    Comment by John — February 24, 2010 @ 12:59 am


RSS feed for comments on this post. TrackBack URI

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

The Shocking Blue Green Theme Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: