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 docstringdefnlog
: automatic logging of function callsdefntry
: automatic catching of exceptions
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:
- either a string or a keyword
- 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.alpha :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:
-
It has not yet been ported to
clojurescript
-
unform
andconform
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 docstringdefprint
: automatic logging of function callsdeftry
: automatic catching of exceptions
Clojure rocks!