whatoncewaslost
whatoncewaslost

Reputation: 2256

Recursive Loop in Clojure via Macro is throwing me errors

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

Answers (1)

coredump
coredump

Reputation: 38789

How will people use your macro?

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.

Recursive macros

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.

Debugging macros

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

Related Questions