Dennis Proksch
Dennis Proksch

Reputation: 260

Why does self-defined macro not work with argument from let?

This macro

(defmacro readn
  [n]
  `(list ~@(repeat n '(read))))

works as I expect if I call it e.g. with

(readn 2) ;; expands to (list (read) (read))

However if I call this macro within a let, it does not work anymore:

(let [N 2] (readn N))
;; CompilerException java.lang.ClassCastException: 
;; clojure.lang.Symbol cannot be cast to java.lang.Number

I have already tried ,n 'n and ~n but this led to no success. I think this is because the N is defined at run-time and the macro works at compile-time?

Putting it in eval like suggested here yet does work:

(let [N 2] (eval `(readn ~N)))
;; expands to (list (read) (read))

I know I could do something similar using loop, yet loop seems (at least to me) kind of terse.

How do I make this macro work and how do I achieve the sketched behaviour, respectively? Thanks!

Upvotes: 1

Views: 71

Answers (1)

Sam Estep
Sam Estep

Reputation: 13304

I think this is because the N is defined at run-time and the macro works at compile-time?

Yes, that's correct. You can think of it this way: when Clojure evaluates a form, it first completely macroexpands the form, then it evaluates that macroexpanded result. Starting with your example:

(let [n 2] (readn n))

Since let is a macro, Clojure expands it:

(macroexpand-1 '(let [n 2] (readn n)))
;=> (let* [n 2] (readn n))

Now let has bottomed out to the special form let*, and the expression for n (2) isn't a macro, so Clojure doesn't need to expand it.

However, the body form (readn n) is a macro form. Remember that macros get passed their arguments unevaluated, so in this case your readn macro literally receives the symbol n.

(macroexpand-1 '(readn n))
;=> java.lang.ClassCastException

Macros are just functions that run at compile-time, so this...

(defmacro readn [n]
  `(list ~@(repeat n '(read))))

(macroexpand-1 '(readn 2))
;=> (clojure.core/list (read) (read))

(macroexpand-1 '(readn n))
;=> java.lang.ClassCastException

... is equivalent to this:

(defn readn [n]
  `(list ~@(repeat n '(read))))

(readn '2)
;=> (clojure.core/list (read) (read))

(readn 'n)
;=> java.lang.ClassCastException

My recommendation would be to just not use a macro at all here. A function would work perfectly well in this case:

(defn readn [n]
  (doall (repeatedly n read)))

(let [n 2] (readn n))

Upvotes: 4

Related Questions