I have some code which I refactored only to find out something was broken with loop. After some debugging I found out loop and with-redefs do not play well together. I realize it may not make sense to use with-redefs inside a loop, but I didn't expect it to not work. I'm not sure if its intentional or not.
This is an MCVE I've created to demonstrate the "problem":
(loop [test 3]
(with-redefs []
(if (zero? test)
"done"
(recur (dec test)))))
This gives me:
Mismatched argument count to recur, expected: 0 args, got: 1
Removing the with-redefs works as expected:
(loop [test 3]
(if (zero? test)
"done"
(recur (dec test))))
and returns "done".
What is the reason the first piece of code does not work? Is this intentional?
The explanation is in the macroexpansion of with-redefs:
(macroexpand-1
'(with-redefs []
(if (zero? test)
"done"
(recur (dec test)))))
returns:
(with-redefs-fn {}
(fn []
(if (zero? test)
"done"
(recur (dec test)))))
where you can see that because a new fn has been introduced, the recur is going to refer to that fn rather than the farther-away loop (which explains the arity exception).
There are a variety of other macros that are "incompatible" with loop in this way, because the recur needs to be in the tail position with respect to loop, and if the recur occurs inside a macro call, the macro may be manipulating the code such that the recur is no longer in tail position.
For with-redefs in particular (and a variety of other situations), a workaround could be:
(loop [test 3]
(let [[recur? val]
(with-redefs []
(if (zero? test)
[false "done"]
[true (dec test)]))]
(if recur?
(recur val)
val)))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With