Custom defn

With clojure.spec, we can parse functions and macros arguments into kind of an Abstract Syntax Tree (AST).

In this two-part series, we are going to show how one can write his custom defn-like macro, using the specs for defn.

In the first part, we showed how one can parse the arguments of the defn macro, modifies the parse tree and converts it back to the format defn expects.

Now, we are going to leverage this idea in order to write a couple of custom defn like macros:

  • defndoc: automatic enrichment of docstring
  • defnlog: automatic logging of function calls
  • defntry: automatic catching of exceptions

Trees

The following pieces of code are inspired form Mark Engleberg better-cond repo.

Requirements

First, we have to require clojure.spec.

I apologise for the fact that it takes a bit of time and might cause page scroll freeze: this is because KLIPSE loads and evaluates code from github while you are reading this article…

(ns my.m$macros
  (:require [clojure.spec.alpha :as s]))

As we explained in the first part, we have to redefine ::defn-args - the spec for defn arguments.

Feel free to skip the following code snippet - and come back to it later. The most important part is the last stament where ::defn-args is defined.

Automatic enrichment of docstring

Let’s say, we want to write a defn like macro with a twist: the docstring will automatically contain the name of the function that is currently defined. Without clojure.spec, you will have to extract manually the optional docstring and reinject it into defn. With clojure.spec, we can do much better by:

  1. Conforming the args into a tree
  2. Modifying the docstring part of the tree
  3. Unforming back

Here is the code in action:

(defmacro defndoc [& args]
  (let [conf (s/conform ::defn-args args)
        name (:name conf)
        new-conf (update conf :docstring #(str name " is a cool function. " %))
        new-args (s/unform ::defn-args new-conf)]
    (cons `defn new-args)))

When no docstring is provided, a docstring is created:

(my.m/defndoc foo [a b] (+ a b))
(:doc (meta #'foo))

When a docstring is provided, a enriched docstring is created:

(my.m/defndoc foo "sum of a and b." [a b] (+ a b))
(:doc (meta #'foo))

This one was pretty easy, because we only had to deal with the docstring. The next one is more challenging - as we are going to deal with the body of the function…

Automatic logging of function calls

defnlog is going to be a macro that defines a function that print a log each time it is called.

In other words, we are going to write a macro that modifies the body of a function. It’s pretty easy, clojure being a homoiconic language: Code is data and it can be manipulated as a regular list.

Our first piece is going to be a function prepend-log that receives a body and a function name and prepend to it a call to (print func-name "has been called):

(defn prepend-log [name body]
  (cons `(println ~name "has been called.") body))

Our second piece is a function update-conf that updates the body of a conformed ::defn-args. This is a bit tricky because the shape of the confomed object is different if the function is a single-arity or a multi-arity function.

Let’s take a look at the shape of a ::defn-args for a single arity function:

(s/conform ::defn-args '(foo [a b] (* a b)))

The body path is: [:bs 1 :body].

And now for a multi-arity function:

(s/conform ::defn-args '(bar 
                          ([] (* 10 12))
                          ([a b] (* a b))))

The bodies path is: [:bs 1 :bodies].

Note that in both cases, the arity type is located at [:bs 0].

Let’s write update-conf:

  • In single-arity, we update the body
  • In multi-arity, we updtate all the bodies

Note how we destructure the conf in order to get the arity.

(defn update-conf [{[arity] :bs :as conf} body-update-fn]
  (case arity
    :arity-1 (update-in conf [:bs 1 :body] body-update-fn)
    :arity-n (update-in conf [:bs 1 :bodies] (fn [bodies]
                                               (map (fn [body] (update body :body body-update-fn)) bodies)))))

All the pieces are ready to write our defnlog macro:

(defmacro defnlog [& args]
  (let [{:keys [name] :as conf} (s/conform ::defn-args args)
        new-conf (update-conf conf (partial prepend-log  (str name)))
        new-args (s/unform ::defn-args new-conf)]
    (cons `defn new-args)))

Let’s see defnlog in action.

First, we define a simple function foo:

(my.m/defnlog foo "aa" [a b] (+ a b))

And when we call it, a log is printed:

(with-out-str (foo [55 200]))

It works fine with destructuring:

(my.m/defnlog baz "aa" [{:keys [a b]}] (+ a b (first c)))
(baz {:a 55 :b 200})
(with-out-str 
(baz {:a 55 :b 200}))

And also with multi-arity functions:

(my.m/defnlog bar 
  ([] (* 10 12))
  ([a b] (* a b)))

(with-out-str
 (bar))
(with-out-str
(bar 12 3))

Automatic try/catch

We can use exactly the same technique to create a defntry macro that wraps the body into a try/catch block - and throws an exception with the name of the function. (It is especially useful in clojurescript with advanced compilation where function names are not available any more at run time!)

First, let’s write a wrap-try function that wraps a body into a try/catch block:

(defn wrap-try [name body]
  `((try ~@body
     (catch :default ~'e
       (throw (str "Exception caught in function " ~name ": " ~'e))))))

And now, the code of the defntry macro:

(defmacro defntry [& args]
  (let [{:keys [name] :as conf} (s/conform ::defn-args args)
        new-conf (update-conf conf (partial wrap-try  (str name)))
        new-args (s/unform ::defn-args new-conf)]
    (cons `defn new-args)))

Let’s see it in action - with a kool function that receives a function and calls it.

(my.m/defntry kool "aa" [a] (a))
(kool #(inc 2))

Now, if we pass something that is not a function, we will get a nice exception with the name of the kool function:

(kool 2)

So beautiful…

And so simple…

Clojure.spec rocks!