Reputation: 2256
I've been trying to write a recursive loop in clojure that will print me out the very last number in the list. The point is not that I need to get the last number (for which I'm sure there's a built in function for that) but that I want to better understand recursion and macros in clojure. So I have this macro...
(defmacro loop-do [the-list]
`(if (= (count '~the-list) 1)
(println (first '~the-list))
(loop-do (rest '~the-list))))
But I get a stackoverflow error. What am I doing wrong?
Upvotes: 1
Views: 244
Reputation: 38789
Somewhere, someone will call:
(loop-do list)
As a piece of code, those are only two symbols in a list. The first one is recognized as your macro, and the second one, list
, is a symbol that represents a variable that will be bound at runtime. But your macro only knows that this is a symbol.
The same goes for:
(loop-do (compute-something))
The argument is a form, but you do not want to get the last element of that form, only the last element of the list obtained after evaluating the code.
So: you only know that in your macro, the-list
will be bound to an expression that, at runtime, will have to be a list. You cannot use the-list
as-if it was a list itself: neither (count 'list)
nor (count '(compute-something))
does what you want.
You could expand into (count list)
or (count (compute-something))
, though, but the result would only be computed at runtime. The job of the macro is only to produce code.
Macros are not recursive: they expand into recursive calls.
(and a b c)
might expand as:
(let [a0 a] (if a0 a0 (and b c)))
The macroexpansion process is a fixpoint that should terminate, but the macro does not call itself (what would that mean, would you expand the code while defining the macro?). A macro that is "recursive" as-in "expands into recursive invocations" should have a base case where it does not expand into itself (independently of what will, or will not, happen at runtime).
(loop-do x)
... will be replaced by:
(loop-do (rest 'x))
... and that will be expanded again. That's why the comments say the size actually grows, and that's why you have a stackoverflow error: macroexpansion never finds a fixpoint.
You have a stackoverflow error. How do you debug that?
Use macroexpand-1, which only performs one pass of macroexpansion:
(macroexpand-1 '(loop-do x))
=> (if (clojure.core/= (clojure.core/count (quote x)) 1)
(clojure.core/println (clojure.core/first (quote x)))
(user/loop-do (clojure.core/rest (quote x))))
You can see that the generated code still contains a call to usr/loop-do
, but that the argument is (clojure.core/rest (quote x))
. That's the symptom you should be looking for.
Upvotes: 2