(str (js/Date.))
Open this presentation in your computer for a collaborative live coding session: http://slides.klipse.tech/clojure-spec-cr17/part1.html#slide-3
(It works also on mobile)
The interactive code snippets are powered by KLIPSE. 🤗
(map inc [1 2 3])
Here is the structure of the args to defn
:
(ns my.m$macros
(:require [clojure.spec.alpha :as s]))
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
?
pre
and post
map - optional
(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:
::binding-form
- required (might be empty)Let's code it:
(s/def ::arg-list
(s/and
vector?
(s/cat :args (s/* ::binding-form)
:varargs (s/? (s/cat :amp #{'&} :form ::binding-form)))))
What is ::binding-form
?
It could be one of:
(s/def ::binding-form
(s/or :sym ::local-name
:seq ::seq-binding-form
:map ::map-binding-form))
::local-name
is any symbol except &
(s/def ::local-name (s/and simple-symbol? #(not= '& %)))
::seq-binding-form
is a vector made of:
::binding-form
(recursive definition)&
followed by a ::binding-form
- optional:as
followed by a ::local-name
- optionalExamples:
[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:
{abc "abc"}
{:keys [a b c] :or {a 0 b 1 c 2}
{:foo/keys [a b c] :as args}
It's a 2-element vector (a.k.a. a tuple) made of:
::map-binding
(pay attention to the recursion!)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"])
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]))
Test
(s/conform ::map-special-binding {:keys '[a b c] :or '{a 1 b 2}})
It must have either keys
or syms
- but qualified
(s/def ::ns-keys
(s/tuple
(s/and qualified-keyword? #(-> % name #{"keys" "syms"}))
(s/coll-of simple-symbol? :kind vector?)))
Test
(s/valid? ::ns-keys [:foo/keys '[a b c]])
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))
Test
(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]})
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))))
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))
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)
::my-spec))
Now they are aligned:
(->> (s/conform ::my-spec-vec [:a 1])
(s/unform ::my-spec-vec))
Don't be afraid by the length of the code 😱
3 stages:
A simple example
(defn foo [[a b]] (+ a b))
We are going to add a docstring using spec
Let's conform the defn
args:
(def conformed-args (s/conform ::defn-args '(foo [[a b]] (+ a b))))
conformed-args
Modify them - adding a docstring:
(def modified-args (assoc conformed-args :docstring "foo is a cool function"))
modified-args
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 😊
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))
Test
(prepend-log 'foo '((print a) (+ a b)))
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)))))
Test
(update-conf (s/conform ::defn-args '(bar
([] (* 10 12))
([a b] (* a b)))) (partial prepend-log 'bar))
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)))
(special-multiplication)
powered by KLIPSE /