Posted on 2017-03-18 Edit on GitHub
with-redefs macro in Clojure is one of the most problematic. The description says:
binding => var-symbol temp-value-expr
Temporarily redefines Vars while executing the body. The temp-value-exprs will be evaluated and each resulting value will replace in parallel the root value of its Var. After the body is executed, the root values of all the Vars will be set back to their old values. These temporary changes will be visible in all threads. Useful for mocking out functions during testing.
The last part about being "useful for mocking out functions during testing" (emphasis mine) has encouraged many users, myself included, to use
with-redefs frequently. However, there are some unfortunate gotchas, one of which I encountered recently.
Take the following:
(defn foo [^long a] "foo") (with-redefs [foo (fn [x] "bar")] (foo "x"))
(foo "x") to return ~"bar"~, but it instead throws an exception:
ClassCastException user$eval20544$fn__20545 cannot be cast to clojure.lang.IFn$LO user/eval20544/fn--20547 (form-init538441322648284404.clj:45)
Getting the Var directly and applying it to ~"x"~ does work, however:
(with-redefs [foo (fn [x] "bar")] ((var foo) "x")) ;; => "bar"
What's going on? As it turns out, the Clojure compiler optimizes functions whose parameters have primitive type hints. The compiler "knows" that
foo takes a
long and returns an
String). By redefining
foo to a function that takes an
Object (the default without a type hint) and returns an
Object, we fail to match types. Invoking the Var of
foo gets around this.
We see that
foo can be redefed with a function that has the same type:
(with-redefs [foo (fn [^long x] "bar")] (foo 1)) ;; => "bar"