10

Custom defn macro with clojure.spec - part 1: conform/unform

 3 years ago
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.
neoserver,ios ssh client

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 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…

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:

  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.

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 docstring
  • defprint: automatic logging of function calls
  • deftry: automatic catching of exceptions

Clojure rocks!

If you enjoy this kind of interactive articles would you consider a (small) donation💸 on Patreon or at least giving a star⭐ for the Klispe repo on Github?

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK