Clojure with-redefs gotcha
Posted on 2017-03-18 Edit on GitHub
The 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"))
We'd expect (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 Object
(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"