Hey there,
I just wanted to share this macro that solves a problem for me when I’m writing tests. I often find that I don’t want to do with-redefs
to replace side-effecting functions because it’s not thread-bound. Instead, I have defined my side-effecting functions as ^:dynamic
, which in turn clutters my code (the functions are not really meant to be dynamic).
So I whipped together this macro that “wraps” the side-effecting function with a check of whether it has been dynamically rebound to another function, and if so calls that function instead.
(defmacro replace-fn
{:clj-kondo/lint-as 'clojure.core/def}
[dyn-var orig-var]
`(do
(def ^:dynamic ~dyn-var nil)
(alter-var-root (var ~orig-var)
(fn [f#]
(fn [& args#]
(if ~dyn-var
(let [x# ~dyn-var]
(binding [~dyn-var nil] ;; EDIT: bind to `nil` instead of `f#`
(apply x# args#)))
(apply f# args#)))))))
It works like this
(defn replace-me
[y]
(* y 2))
;; ^:dynamic is strictly speaking not
;; needed here but it prevents linting errors
(replace-fn ^:dynamic *replacement* replace-me)
(replace-me 2)
;; => 4
(binding [*replacement* (fn [y]
(* 3 y))]
(replace-me 2))
;; => 6
;; Inside the replacement function, the original function name
;; refers to the original function, not its replacement
(binding [*replacement* (fn [y]
(replace-me (* 3 y)))]
(replace-me 2))
;; => 12
Hope someone finds this useful!