Parsing

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 this part, we are going to show how one can parse the arguments of the defn macro, modifies the parse tree and converts it back to the format defn expects.

In the second part, we will 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

Tree

conform and unform

The basic idea of this article is based on the fact that in clojure.spec, conform and unform are reciprocical.

Here is the documentation of unform:

Usage: (unform spec x)
Given a spec and a value created by or compliant with a call to
'conform' with the same spec, returns a value with all conform
destructuring undone.

In other words: (unform spec (conform spec x)) is equal to x.

Let’s play with conform/unform with a simple spec - that receives a list that contains two elements:

  1. either a string or a keyword
  2. a number

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.spec
        (:require [clojure.spec :as s]))
(s/def ::str-or-kw (s/alt :str string?
                          :kw  keyword?))

(s/def ::my-spec (s/cat
                   :first ::str-or-kw
                   :second number?))

Let’s look how conform destrucutres valid input:

(s/conform ::my-spec '(:a 1))

And when we call unform, we get the original data back:

(->> (s/conform ::my-spec '(:a 1))
     (s/unform ::my-spec))

Catches with conform/unform

Sometimes conform and unform are not fully inlined.

Take a look at this:

(->> (s/conform ::my-spec [:a 1])
     (s/unform ::my-spec))

[:a 1] is a valid ::my-spec but it is unformed as a list and not as a vector.

One way to fix that is to use spec/conformer, like this:

(s/def ::my-spec-vec (s/and vector?
                        (s/conformer vec vec)
                                                (s/cat
                                                                          :first ::str-or-kw
                                                                                                    :second number?)))
(->> (s/conform ::my-spec-vec [:a 1])
     (s/unform ::my-spec-vec))

Now, let’s move to the defn stuff…

args of defn macro

The spec for defn arguments is :clojure.core.specs/defn-args and it is defined in here.

But there are two problems with this implementation:

  1. It has not yet been ported to clojurescript

  2. unform and conform are not fully inlined (unform returns lists instead of vectors).

Here is the full spec for :defn-args where unform and conform are fully inlined. This code is inspired form Mark Engleberg better-cond repo.

It is composed of a lot of specs; feel free to skip this code snippet - and come back to it later. The most important part is the last stament where ::defn-args is defined.

As you can see, defn is a pretty complex macro that deals with a lot of arguments combinations and options. Before clojure.spec, it was really hard to write defn like macros. But now, it’s much easier…

Now, let’s see ::defn-args in action.

First, with a simple function foo:

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

And now, with a multi-arity variadic function that provides a docstring and meta data.

(s/conform ::defn-args '(bar "bar is a multi-arity variadic function" {:private true} ([a b & c] (+ a b (first c))) ([] (foo 1 1))))

The cool thing is that we can manipulate the AST - returned by conform. For instance, we can modify the docstring:

(def the-new-args-ast 
  (-> (s/conform ::defn-args '(bar "bar is a multi-arity variadic function" {:private true} ([a b & c] (+ a b (first c))) ([] (foo 1 1))))
        (assoc :docstring "bar has a cool docstring")))

And if we unform it, we get:

(s/unform ::defn-args the-new-args-ast)

We can now, create a defn statement with the modified arguments:

(cons `defn (s/unform ::defn-args the-new-args-ast))

In our next article, we will use those ideas to create custom defn like macros.

  • defdoc: automatic enrichment of docstring
  • defprint: automatic logging of function calls
  • deftry: automatic catching of exceptions

Clojure rocks!