morphe.core
In Clojure's grammar, defn
forms have many optional and variadic terms (metadata, docstrings, single-arity vs. multi-arity structure, etc.). The main work of defn
is to parse the body of the macro, extracting all these terms; afterward its job is simply to write these out in a more restricted form to be compiled.
morphe
works by forking clojure.core/defn
, splitting it into these two fundamental components: the parser and writer. The forked parser outputs a FnDef
record which is consumed by the writer. But between being parsed and being compiled, this record can easily be examined and/or modified by aspect-defining functions.
#_=> (defn spied [fn-def] (m/alter-bodies fn-def `(let [r# (do ~@&body)] (println r#) r#)))
#_=> (m/defn ^{::m/aspects [spied]} foo ([x] (inc x)) ([x y] (+ x y)))
#_=> (= 2 (foo 1))
;; prints: "2"
true
#_=> (= 5 (foo 2 3))
;; prints "5"
true
In the example above, ^{::m/aspects [...]}
tells Clojure's reader to attach a map of metadata to a symbol. morphe.core/defn
parses the function definition as normal, then examines the symbol's metadata to determine which aspects it is tagged with. This is all standard Clojure stuff. As an aside, an alternate style is available that is preferable in a source file:
;; The metadata map immediately precedes the `defn` block.
^{::m/aspects [spied]}
(m/defn foo ([x] (inc x)) ([x y] (+ x y)))
In any case, Morphe then exploits the fact that the compiler itself is a Clojure process, and reduces the tagged aspect functions over the parsed form (in this case, just spied
). Once all such tags have been applied, the result is passed along to the writer, just as clojure.core/defn
implicitly would have done.
It is fairly straightforward to modify the FnDef
record directly. But morphe.core
provides a number of conveniences to make writing common aspect transformations as simple as possible; examples include wrapping the whole definition (perhaps in the body of a let
), or prefixing every body of the function (perhaps with generated log statements). Let us consider in more depth the definition of a simple trace-level logging transformation:
(defn traced
"Inserts a log/trace call as the first item in the fn body, recording
the fully-qualified function name and the arity called."
[fn-def]
(m/prefix-bodies fn-def
`(log/trace "calling function: "
~(format "%s/%s:%s" (ns-name &ns) &name &arglist)))))
This is equivalent to the following method, which does not use any convenience functions and instead modifies the FnDef
record directly:
(defn traced
"Inserts a log/trace call as the first item in the fn body, recording
the fully-qualified function name and the arity called."
[fn-def]
(let [namespaced-fn-name (format "%s/$s"
(str (ns-name (:namespace fn-def)))
(str (:fn-name fn-def)))]
(assoc fn-def :bodies
(for [[body args] (apply map list ((juxt :bodies :arglists) fn-def))]
(conj body `(log/trace ~(format "calling function: %s:%s"
namespaced-fn-name
args)))))))
If you have never written Clojure macros, there are a very few tricky things to this process. The community is helpful, and help is also available in Clojure for the Brave and True and of course Paul Graham's On Lisp. If you want to dive very deep and have a strong stomach for ebullient superlatives, I recommend Let over Lambda.
morphe.core
utilitiesClojure's defmacro
is an anaphoric macro. Code inside defmacro
has access to two special variables, &env
and &form
. &env
is, from the documentation, "a map of local bindings at the point of macro expansion. The env map is from symbols to objects holding compiler information about that binding." &form
is "the actual form (as data) that is being invoked."
Morphe's convenience utilities are also anaphoric macros. Depending on the utility, some of the following variables are available:
&ns
: the namespace in which the aspect-modified function is being defined.&name
: the unqualified name given to the function.&env-keys
: the keyset of the &env
map as seen by the morphe.core/defn
macro itself (i.e., set of symbols bound in a local scope)&meta
: the metadata with which the function has been tagged&arglist
: the paramaters vector for a particular arity of the function.&body
: the collection of expression(s) constituting a particular arity of the function.&form
: an uninspectable representation of the collection of expressions for the entire function declaration; useful to wrap the whole defn
with a lexical scope.defn
A drop-in replacement for Clojure's defn
. In the simple case, the two should be indistinguishable. But you can tag the fn-name with metadata, under the keyword :morphe.core/aspects
, to trigger the application of aspects.
prefix-form: [fn-def expression]
Anaphoric macro, providing &ns
, &name
, &env-keys
, and &meta
.
This will prefix the entire form with the provided expression. Example:
(prefix-form
fn-def
`(def gets-defined-first 3))
alter-form: [fn-def expression]
Anaphoric macro, providing &ns
, &name
, &env-keys
, &meta
, and &form
.
This will wrap the entire form, with the form's location in the code specified by &form
. &form
must be assumed to be a single valid expression, not a sequence of expressions.
Example:
(alter-form fn-def
`(binding [*my-var* 3] &form))
prefix-bodies: [fn-def expression]
Anaphoric macro, providing &ns
, &name
, &env-keys
, &meta
, and &arglist
.
This will prefix each body of the function with the provided expression. &arglist
will evaluate to the parameter list corresponding to each body.
Example:
(prefix-bodies fn-def
`(assert (even? 4)
(format "Math still works in the %s arity."
'~&arglist)))
alter-bodies: [fn-def expression]
Anaphoric macro, providing &ns
, &name
, &env-keys
, &meta
, &arglist
, and &body
.
For each arity of the function, this replaces the clauses with the given expression; &arglist
and &body
are bound appropriately for each arity, and &body
is assumed to be a sequence of valid expressions, not a single valid expression. Typically used for wrapping each body somehow.
Example:
(alter-bodies fn-def
`(binding [*some-scope* ~{:ns &ns,
:sym &name,
:arity &arglist}]
~@&body))
Let's say you want to log every time a method is called, along with the arity. Usually you want this to be at the warn level, but sometimes you want debug or info.
(defn logged
"Higher order fn, returning an aspect fn. Inserts a log call as the
first item in each fn body."
([] (logged :warn))
([level]
(fn [fn-def]
(d/prefix-bodies
fn-def
`(log/log ~level
~(format "Logging at %s level: Entering fn %s/%s:%s."
level
&ns
&name
&arglist))))))
;; Now let's use it:
^{::m/aspects [(logged :debug)]}
(m/defn my-logged-fn
([x] x)
([x y] (+ x y))
([x y z] (+ x y z))
([x y z & more] (apply + x y z more)))
;; This expands to:
(defn my-logged-fn
([x]
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-logged-fn:[x].")
x)
([x y]
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-logged-fn:[x y].")
(+ x y))
([x y z]
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-logged-fn:[x y z].")
(+ x y z))
([x y z & more]
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-logged-fn:[x y z & more].")
(apply + x y z more)))
Now suppose you want to time a function.
(defn timed
"Creates a lexical scope for the defn with a codehale timer defined, which is
then used to time each function call."
[fn-def]
(let [timer (gensym 'timer)]
(-> fn-def
(d/alter-form `(let [~timer (metrics/timer ~(format "Timer for the function: %s"
(symbol (str &ns) &name)))]
(metrics/register metrics/DEFAULT ~[(str &ns) (str &name) "timer"])
~&form))
(d/alter-bodies `(metrics/with-timer ~timer ~@&body)))))
;; Let's use it:
^{::m/aspects [timed]}
(m/defn my-timed-fn
([x] x)
([x y] (+ x y))
([x y z] (+ x y z))
([x y z & more] (apply + x y z more)))
;; This expands to:
(let [timer7068 (metrics/timer "Timer for the function: my-ns/my-timed-fn")]
(metrics/register metrics/DEFAULT ["my-ns" "my-timed-fn" "timer"])
(defn my-timed-fn
([x]
(metrics/with-timer timer7068
x))
([x y]
(metrics/with-timer timer7068
(+ x y)))
([x y z]
(metrics/with-timer timer7068
(+ x y z)))
([x y z & more]
(metrics/with-timer timer7068
(apply + x y z more))))
Let's do both.
^{::m/aspects [timed (logged :debug)]}
(m/defn my-amazing-fn
([x] x)
([x y] (+ x y))
([x y z] (+ x y z))
([x y z & more] (apply + x y z more)))
;; This expands to:
(let [timer7068 (metrics/timer "Timer for the function: my-ns/my-amazing-fn")]
(metrics/register metrics/DEFAULT ["my-ns" "my-amazing-fn" "timer"])
(defn my-amazing-fn
([x]
(metrics/with-timer timer7068
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x].")
x))
([x y]
(metrics/with-timer timer7068
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y].")
(+ x y)))
([x y z]
(metrics/with-timer timer7068
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y z].")
(+ x y z)))
([x y z & more]
(metrics/with-timer timer7068
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y z & more].")
(apply + x y z more))))
Aspects are applied in composition order (right to left). Change the aspects' order in the tagged vector, and you change the order of application:
^{::m/aspects [(logged :debug) timed]}
(m/defn my-amazing-fn
([x] x)
([x y] (+ x y))
([x y z] (+ x y z))
([x y z & more] (apply + x y z more)))
;; This expands to:
(let [timer7068 (metrics/timer "Timer for the function: my-ns/my-amazing-fn")]
(metrics/register metrics/DEFAULT ["my-ns" "my-amazing-fn" "timer"])
(defn my-amazing-fn
([x]
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x].")
(metrics/with-timer timer7068
x))
([x y]
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y].")
(metrics/with-timer timer7068
(+ x y)))
([x y z]
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y z].")
(metrics/with-timer timer7068
(+ x y z)))
([x y z & more]
(log/log :debug "Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y z & more].")
(metrics/with-timer timer7068
(apply + x y z more))))
In the examples so far, similar effects could be achieved via (possibly clunky) functional composition (see morphe's utilities for such here). There are some limitations: the automatic exposure of &name
or &arglist
is not possible via purely functional means. But the fact that we are operating on the function's code rather than the function itself does allow even more interesting transformations one could not effect purely functionally. Consider this funny little example I once used in practice (observe carefully how some of the code gets restructured in the second arity):
^{::m/aspects [(synchronize-on state #{pojo-1 pojo-2})]}
(m/defn safely-update-then-calculate
([state pojo-1]
(when-let [x (.inspect pojo-1)]
(.update pojo-1 (:one @state))
(expensive-calculation x))
([state pojo-1 pojo-2]
(when-let [x (.inspect pojo-1)]
(.update pojo-1 (:one @state))
(.update pojo-2 (:two @state))
(expensive-calculation x))))
;; expands to:
(defn safely-update-then-calculate
([state pojo-1]
(when-let [x (locking state
(when-let [x46735 (.inspect pojo-1)]
(.update pojo-1 (:one @state))
x46735))]
(expensive-calculation x)))
([state pojo-1 pojo-2]
(when-let [x (locking state
(when-let [x46736 (.inspect pojo-1)]
(.update pojo-1 (:one @state))
(.update pojo-2 (:two @state))
x46736))]
(expensive-calculation x))))
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close