Clojure.Spec Workshop
Part 3 - How to write defn-like macros

qr code
ClojureRemote - February 2017
Yehonathan Sharvit

		  (str (js/Date.))

Who am I?

  • A mathematician
  • A pragmatic theorist
  • A coder
  • A freak of interactivity
  • Founded Audyx - an Audiology Startup with 30K LOCs in Clojurescript
  • Author of KLIPSE
  • A Clojure consultant


  • specs for defn args
  • Conform/Unform loop
  • defn-like macros

KLIPSE - Code interactivity

Open this presentation in your computer for a collaborative live coding session:

(It works also on mobile)

qr code

The interactive code snippets are powered by KLIPSE. 🤗

	    (map inc [1 2 3])

defn args

Here is the structure of the args to defn:

Let's code it:

	      (s/def ::defn-args
	      (s/cat :name simple-symbol?
              :docstring (s/? string?)
              :meta (s/? map?)
              :bs (s/alt :arity-1 ::args+body
              :arity-n ::multi-args+body)))

What is ::args+body?

  • A list of args - required
  • A pre and post map - optional
  • A body - required (might be empty)

		(s/def ::args+body
		(s/cat :args ::arg-list
		:prepost (s/? map?)
		:body (s/* any?)))

What is ::multi-args+body?

A list of bodies

	      (s/def ::multi-args+body (s/cat :bodies (s/+ (s/spec ::args+body))))


What is ::arg-list?

A vector made of:

Let's code it:

	      (s/def ::arg-list
	      (s/cat :args (s/* ::binding-form)
              :varargs (s/? (s/cat :amp #{'&} :form ::binding-form)))))

What is ::binding-form?

It could be one of:

  • Any symbol (except &)
  • A sequential destructuring form
  • A map destructuring form

	      (s/def ::binding-form
	      (s/or :sym ::local-name
              :seq ::seq-binding-form
              :map ::map-binding-form))

::binding-form (cont.)

::local-name is any symbol except &

	    (s/def ::local-name (s/and simple-symbol? #(not= '& %)))

::seq-binding-form is a vector made of:

  • A sequence of ::binding-form (recursive definition)
  • A & followed by a ::binding-form - optional
  • A :as followed by a ::local-name - optional


  • [x y z]
  • [x y & z]
  • [x y :as args]

Not so hard to code

	      (s/def ::seq-binding-form
	      (s/and vector?
              (s/cat :elems (s/* ::binding-form)
              :rest (s/? (s/cat :amp #{'&} :form ::binding-form))
              :as (s/? (s/cat :as #{:as} :sym ::local-name)))))


::map-binding-form is a hybrid spec. It is a map with a repetition of either:

Regular map destructuring

It's a 2-element vector (a.k.a. a tuple) made of:

  • a ::map-binding (pay attention to the recursion!)
  • anything

Easy to to code

	      (s/def ::map-binding (s/tuple ::binding-form any?))

Test (we have to wait until :map-binding is fully defined)

	      (s/conform ::map-binding '[abc "abc"])

::map-binding-form (cont.)

Special map destructuring

The optional keys are keys, syms, strs, as and or

A bit hard to code

	      (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/conform ::map-special-binding {:keys '[a b c] :or '{a 1 b 2}})

::map-binding-form (cont.)

Namespaced keywords map destructuring

It must have either keys or syms - but qualified

	      (s/def ::ns-keys
	      (s/and qualified-keyword? #(-> % name #{"keys" "syms"}))
	      (s/coll-of simple-symbol? :kind vector?)))


	      (s/valid? ::ns-keys [:foo/keys '[a b c]])

::map-binding-form (last)

Combining all the pieces together

We need to combine the three kinds of specs

And prevent keys other thant the allowed ones

	      (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))


	      (s/conform ::map-bindings '{a "aa"})

	      (s/conform ::map-bindings '{:syms [a b c]})

	      (s/conform ::map-bindings '{a "aa"
              {:xx/keys [a b x]} "x"})

Invalid keys are not allowed

	      (s/conform ::map-bindings '{:kseys [a b x]})

Testing defn-args

Simple function

	    (s/conform ::defn-args '(foo [x y _] (+ x y)))

A bit of destructuring

		(s/conform ::defn-args '(bar [[x y] {:keys [a b]}] (+ x y a b)))

Multi-arity function - with a docstring and metadata

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

Conform and unform

With conform you parse your data

	    (s/def ::my-spec (s/cat
            :first (s/alt :str string?
            :kw  keyword?)
	    :second number?))
	    (s/conform ::my-spec '(:a 1))	    

With unform you get your data back:

	      (s/unform ::my-spec {:first [:kw :a], :second 1})

Usually, you can close the loop

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

Conform and unform - bugs and tricks

Sometimes conform and unform are not aligned:

	    (s/def ::a-tuple (s/and vector? (s/* any?)))
	    (->> (s/conform ::a-tuple [:a 1])
	    (s/unform ::a-tuple))

Fix with custom conformer:

	      (s/def ::my-spec-vec (s/and vector?
              (s/conformer vec vec)

Now they are aligned:

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

spec for defn args - unform friendly

Don't be afraid by the length of the code 😱

Modifying the code of a function

3 stages:

  1. conform the args
  2. modify the conformed data
  3. unform back

A simple example

	      (defn foo [[a b]] (+ a b))

We are going to add a docstring using spec

Adding a docstring

Let's conform the defn args:

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

Modify them - adding a docstring:

	      (def modified-args (assoc conformed-args :docstring "foo is a cool function"))

Unform the modified args:

	      (s/unform ::defn-args modified-args)

Put back into defn:

		(defn foo "foo is a cool function" [[a b]] (+ a b))


Let's write our own defndoc macro.

defndoc is like defn but it prepends to the docstring a sentence that contains the name of the function

		(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)))

Let's see it in action.

When no docstring is provided

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

When a docstring is provided

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

Too easy for you?

Let's do something much more fun.


defnlog is like defn but it prints a log each time it is called

Now we have to modify the body of the original function

No worries! In clojure, code is data 😊

First piece: prepend-log

prepend-log receives a body and a function name and prepends to the body a call to (println func-name "has been called"):

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


		(prepend-log 'foo '((print a) (+ a b)))

Second piece: update-conf

update-conf 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 has single-arity or multi-arity.

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

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

But we can do it 💪

	      (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)))))


	      (update-conf (s/conform ::defn-args '(bar 
              ([] (* 10 12))
              ([a b] (* a b)))) (partial prepend-log 'bar))

The Grand Finale

The Grand Finale

And now, for the grand finale, please welcome defnlog!!!!

	    (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 it in action

First, with a single arity function

	      (my.m/defnlog trivial-multiplication "Multiplies `a` and `b`" [a b] (* a b))

Are you ready?

	      (trivial-multiplication 13 4)

And now, with a multi-arity function

	      (my.m/defnlog special-multiplication
	      "Multiplies `a` and `b` with a tweak"
	      ([] (* 12 56))
	      ([a] (* a 42))
	      ([a b] (* a b)))




Want more?

Come back next year!

powered by KLIPSE /