title | date | draft |
---|---|---|
Clojure Debugging Helpers |
2022-11-11 10:53:47 +0100 |
false |
I highly recommend Repl Driven Development by Stuart Halloway if you haven't watched it already. It's a real eye opener when it comes to the topic of REPL and how to use it to your advantage. Two sentence summary of that presentation could easily be:
Don't type into REPL! Send things to the REPL!
As soon as you begin to understand that message, a whole new world opens up where some things become so incredibly simple. For example, how to debug a misbehaving ring handler?
(defn handler [request]
,,,)
Just make a binding that can be manipulated. Then reevaluate the handler (send
updated defn
to the REPL).
(defn handler [request]
(def request request)
,,,)
When the new handler executes, request
becomes available for prodding as a
top-level var. What can you do with it? Well, what do you need to investigate
the problem? Send more things to the REPL. Here are a couple of examples:
(-> request :params :user-id)
(-> request :body :what :does :not :look :right?)
(get-in request [:headers "user-agent"])
(reitit-ring/get-match request)
(-> request (reitit-ring/get-match) :data (get (:request-method request)))
(db/get-user (-> request :params :user-id))
This technique is so general that it can be used almost everywhere.
- with
defn-
,let
,letfn
,if-let
,when-let
,binding
,with-redefs
, ... - in
src
ortest
It does get a bit tedious sometimes. E.g. when needing to litter the code
(temporarily) with a bunch of def
expressions.
(defn handler [{:keys [params body] :as request}]
(def request request)
(def params params)
(def body body)
(let [db (:db/admin request)
user-id (:user-id params)
user (db/get-user db user-id)]
(def db db)
(def user-id user-id)
(def user user)
(do-something user body)))
Wouldn't it be nice if I could specify that I want to add def
expressions for
every binding without having to type (or
expand)
too much? How about something like the following?
(defn/d handler [{:keys [params body] :as request}]
(let/d [db (:db/admin request)
user-id (:user-id params)
user (db/get-user db user-id)]
(do-something user body)))
Notice the variants defn/d
and let/d
instead of normal clojure.core/defn
and clojure.core/let
. What would it take for this to work? Here is what I
ended up with and am quite happy using it.
The two helper namespaces are in the repl
directory. 1
repl/defn.clj
(ns defn)
(defn symbols [args]
(mapcat
(fn [a]
(cond
(map? a) (:keys a)
(vector? a) (symbols a)
:else [a]))
args))
(defmacro d [fn-name & fdecl]
(let [[args body] (if (string? (first fdecl))
[(second fdecl) (not-empty (drop 2 fdecl))]
[(first fdecl) (not-empty (rest fdecl))])
defs (map (fn [s] `(def ~s ~s)) (symbols args))]
`(defn ~fn-name ~args
~@defs
~@body)))
repl/let.clj
(ns let)
(defmacro d [bindings & body]
(let [bindings-with-defs (->> bindings
(destructure)
(partition 2)
(mapcat (fn [[s code]]
[s code
(gensym "not-used") `(def ~s ~s)])))]
`(let [~@bindings-with-defs]
~@body)))
For the helpers to be usable elsewhere in the project I made sure the following was evaluated every time I started the project in the REPL:
(load-file "repl/let.clj")
(load-file "repl/defn.clj")
That's it! Now I can use those helpers so I don't have to write a bunch of
def
expressions anymore.
(defn/d handler [{:keys [params body] :as request}]
(let/d [db (:db/admin request)
user-id (:user-id params)
user (db/get-user db user-id)]
(do-something user body)))
-
If you don't want to use
load-file
, you can put the helpers on the classpath andrequire
them. -
If you don't have monorepo then you need to decide where it makes the most sense to put the helpers, but
load-file
might still be your best way of including the code in the project. -
If you don't like that the helpers are in a separate namespace then you can add them to
clojure.core
and use them like any otherclojure.core
fn.(do (in-ns 'clojure.core) (defmacro defnd ,,,) (defmacro letd ,,,))
-
The helpers above are not perfect, but are quite adequate. For example, I didn't have a strong need to create macros to cover
defn-
,letfn
,if-let
,when-let
,binding
,with-redefs
, ordefn
with additional metadata. Those usages are rare enough that I can adddef
expressions manually. Feel free to develop your own helpers that match the situation you're in.
Footnotes
-
For complete files see: https://gist.github.com/mbezjak/0f11c0b550a0751c66903947328f947c ↩