Custom defn macro with clojure.spec - part 1: conform/unform
source link: https://blog.klipse.tech/clojure/2016/10/10/defn-args.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Custom defn macro with clojure.spec - part 1: conform/unform
Oct 10, 2016 • Yehonathan Sharvit
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…
xxxxxxxxxx
(ns my.spec
(:require [clojure.spec.alpha :as s]))
the evaluation will appear here (soon)...
xxxxxxxxxx
(s/def ::str-or-kw (s/alt :str string?
:kw keyword?))
(s/def ::my-spec (s/cat
:first ::str-or-kw
:second number?))
xxxxxxxxxx
the evaluation will appear here (soon)...
Let’s look how conform
destrucutres valid input:
xxxxxxxxxx
(s/conform ::my-spec '(:a 1))
xxxxxxxxxx
the evaluation will appear here (soon)...
And when we call unform
, we get the original data back:
xxxxxxxxxx
(->> (s/conform ::my-spec '(:a 1))
(s/unform ::my-spec))
xxxxxxxxxx
the evaluation will appear here (soon)...
Catches with conform/unform
Sometimes conform
and unform
are not fully inlined.
Take a look at this:
xxxxxxxxxx
(->> (s/conform ::my-spec [:a 1])
(s/unform ::my-spec))
xxxxxxxxxx
the evaluation will appear here (soon)...
[: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:
xxxxxxxxxx
(s/def ::my-spec-vec (s/and vector?
(s/conformer vec vec)
(s/cat
:first ::str-or-kw
:second number?)))
xxxxxxxxxx
the evaluation will appear here (soon)...
xxxxxxxxxx
(->> (s/conform ::my-spec-vec [:a 1])
(s/unform ::my-spec-vec))
xxxxxxxxxx
the evaluation will appear here (soon)...
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.
xxxxxxxxxx
;loaded from gist: https://gist.github.com/viebel/ab64ed95820af42b366889a872dc28ac
(s/def ::local-name (s/and simple-symbol? #(not= '& %)))
(s/def ::binding-form
(s/or :sym ::local-name
:seq ::seq-binding-form
:map ::map-binding-form))
;; sequential destructuring
(s/def ::seq-binding-form
(s/and vector?
(s/conformer identity vec)
(s/cat :elems (s/* ::binding-form)
:rest (s/? (s/cat :amp #{'&} :form ::binding-form))
:as (s/? (s/cat :as #{:as} :sym ::local-name)))))
;; map destructuring
(s/def ::keys (s/coll-of ident? :kind vector?))
(s/def ::syms (s/coll-of symbol? :kind vector?))
(s/def ::strs (s/coll-of simple-symbol? :kind vector?))
(s/def ::or (s/map-of simple-symbol? any?))
(s/def ::as ::local-name)
(s/def ::map-special-binding
(s/keys :opt-un [::as ::or ::keys ::syms ::strs]))
(s/def ::map-binding (s/tuple ::binding-form any?))
(s/def ::ns-keys
(s/tuple
(s/and qualified-keyword? #(-> % name #{"keys" "syms"}))
(s/coll-of simple-symbol? :kind vector?)))
(s/def ::map-bindings
(s/every (s/or :mb ::map-binding
:nsk ::ns-keys
:msb (s/tuple #{:as :or :keys :syms :strs} any?)) :into {}))
(s/def ::map-binding-form (s/merge ::map-bindings ::map-special-binding))
;; bindings
(s/def ::binding (s/cat :binding ::binding-form :init-expr any?))
(s/def ::bindings (s/and vector? (s/* ::binding)))
;; defn, defn-, fn
(defn arg-list-unformer [a]
(vec
(if (and (coll? (last a)) (= '& (first (last a))))
(concat (drop-last a) (last a))
a)))
(s/def ::arg-list
(s/and
vector?
(s/conformer identity arg-list-unformer)
(s/cat :args (s/* ::binding-form)
:varargs (s/? (s/cat :amp #{'&} :form ::binding-form)))))
(s/def ::args+body
(s/cat :args ::arg-list
:prepost (s/? map?)
:body (s/* any?)))
(s/def ::defn-args
(s/cat :name simple-symbol?
:docstring (s/? string?)
:meta (s/? map?)
:bs (s/alt :arity-1 ::args+body
:arity-n (s/cat :bodies (s/+ (s/spec ::args+body))
:attr (s/? map?)))))
xxxxxxxxxx
the evaluation will appear here (soon)...
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
:
xxxxxxxxxx
(s/conform ::defn-args '(foo [[a b]] (+ a b)))
xxxxxxxxxx
the evaluation will appear here (soon)...
And now, with a multi-arity variadic function that provides a docstring and meta data.
xxxxxxxxxx
(s/conform ::defn-args '(bar "bar is a multi-arity variadic function" {:private true} ([a b & c] (+ a b (first c))) ([] (foo 1 1))))
xxxxxxxxxx
the evaluation will appear here (soon)...
The cool thing is that we can manipulate the AST - returned by conform. For instance, we can modify the docstring:
xxxxxxxxxx
(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")))
xxxxxxxxxx
the evaluation will appear here (soon)...
And if we unform it, we get:
xxxxxxxxxx
(s/unform ::defn-args the-new-args-ast)
xxxxxxxxxx
the evaluation will appear here (soon)...
We can now, create a defn
statement with the modified arguments:
xxxxxxxxxx
(cons `defn (s/unform ::defn-args the-new-args-ast))
xxxxxxxxxx
the evaluation will appear here (soon)...
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!
to stay up-to-date with the coolest interactive articles around the world.
Discover more cool interactive articles about javascript, clojure[script], python, ruby, scheme, c++ and even brainfuck!
Give Klipse a Github star to express how much you appreciate Code Interactivity.
Subscribe to the Klipse newsletter:Feel free to email me [email protected] for getting practical tips and tricks in writing your first interactive blog post.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK