Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure macro puzzle: expanding sequence in macro args

Tags:

clojure

This is not my 'production code' but a simplication of the problem for illustration purposes. Also, the title of this question is misleading because it brings to mind the ~@ expansion, which I understand, and which may not necessarily be the problem. Please suggest a better question title if you can.

Given a macro with the following form:

(defmacro my-add [x & ys] `(+ ~x ~@ys))

Now let's say we have a list:

(def my-lst '(2 3))

Now I want a function that uses my-add that I can pass my-lst to as an arg, i.e.

(call-my-add 1 my-lst)

I define the function in what would seem to be the obvious way:

(defn call-my-add [x ys]
    (apply my-add (cons x ys)))

But:

java.lang.Exception: Can't take value of a macro: #'user/call-my-add (repl-1:60)

I've tried all sorts of wild tricks to get the call-my-add function to work using evals, applies, and even defining call-my-add as a macro, but they all give similar ClassCastExceptions.

Is there any way out of this?

like image 426
jk. Avatar asked Feb 25 '23 20:02

jk.


2 Answers

No. Macros do not, cannot, will never, have access to the actual run-time values contained in their arguments, so cannot splice them into the expansion. All they get is the symbol(s) you pass to them, in this case my-list. The "way around this" is to define my-add as a function, and then (optionally) have a macro that calls that function in order to generate its code.

I wrote a blog post about this semi-recently that you might find enlightening.

You could do it with evals if you wanted to, but that is a terrible idea in almost every case:

(let [my-list '(1 2)]
  (eval `(my-add 5 ~@my-list)))
like image 130
amalloy Avatar answered Feb 27 '23 09:02

amalloy


What a great example showing that macros are not first class citizens in Clojure (or any Lisp that I know of). They cannot be applied to functions, stored in containers, or passed to functions etc. In exchange for this they get to control when and if their arguments are evaluated.

What happens at macro expansion time must stay in macro expansion time. So if my-add is being evaluated at macro expansion time and you want to use apply then you need... another macro; to do the applying.

(defmacro call-my-add [x ys]
   `(my-add ~@(cons x ys))) 

Macros are somewhat contagious in this way.

PS: I'm not at my repl so please edit if you see a bug in this example (or I'll fix it when I get back)

like image 37
Arthur Ulfeldt Avatar answered Feb 27 '23 08:02

Arthur Ulfeldt