morphe.core
In Clojure's grammar, defn
forms have many optional and variadic terms. The main work of defn
is to parse the body of the macro, extracting all these terms; in the end 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 its two fundamental components: the parser and writer. The new 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.
Morphe then 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; for example, wrapping the whole definition (perhaps in the body of a let
), or prefixing every body of the function (perhaps with generated log statements). For instance, defining a simple trace-level logging transformation is easy:
(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 ¶ms)))))
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 run.&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¶ms
: 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. morphe.core/defn
first calls parse-defn
, then applies the tagged aspects in order, then calls fn-def=>defn
.
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 ¶ms
.
This will prefix each body of the function with the provided expression. ¶ms
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."
'~¶ms)))
alter-bodies: [fn-def expression]
Anaphoric macro, providing &ns
, &name
, &env-keys
, &meta
, ¶ms
, and &body
.
For each arity of the function, this replaces the clauses with the given expression; ¶ms
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 ¶ms}]
~@&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
¶ms))))))
;; Now let's use it:
(m/defn ^{::m/aspects [(logged :debug)]}
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/defn ^{::m/aspects [timed]}
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/defn ^{::m/aspects [timed (logged :debug)]}
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/defn ^{::m/aspects [(logged :debug) timed]}
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 ¶ms
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/defn ^{::m/aspects [(synchronize-on state #{pojo-1 pojo-2})]}
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