Question

Consider the following function

(defn shove [data fun] (eval `(-> ~data ~fun)))

which works as expected here

(shove [1 2 3] count)  ;; ~~> 3

and even here, where it expectedly fails, because it evaluates (count) too early

(shove [1 2 3] (count))
;; ~~> clojure.lang.Compiler$CompilerException: clojure.lang.ArityException: 
;;       Wrong number of args (0) passed to: core$count, compiling:(null:5:1)

But here, when I define an explicit form and pass it to the function as data, all is well:

(def move '(count))
(shove [1 2 3] move)  ;; ~~> 3

Now, in an attempt to get rid of the explicit call of eval, I try

(defmacro shovem [data form] `(-> ~data ~form))

which works fine

(shovem [1 2 3] count)    ;; ~~> 3
(shovem [1 2 3] (count))  ;; ~~> 3

but it now unexpectedly fails on the explicitly defined form move, with an error that suggests that it evaluates move to get (count) and then keeps trying to evaluate (count), but in a different way than before.

(shovem [1 2 3] move)  
;; ~~> java.lang.ClassCastException: 
;; clojure.lang.PersistentList cannot be cast to clojure.lang.IFn

I'm confused about this error message and I don't know how to get the desired behavior, which is that shovem should work on all three kinds of input, bare functions like count, parenthesized function forms like (count), and data objects like move that evaluate to such forms.

I'm ok with using eval in the function version, but, at this point, I realize I don't understand what's going on and I want to complete the exercise so as to improve my understanding.

Was it helpful?

Solution

A solution?

In the most general case, you'd need both a macro and eval for this purpose of this exercise, which I assume is for learning (please don't actually do this).

For the sake of example, keep shove as is, and use as a helper to a modified shovem

(defn shove [x form] (eval `(-> ~x ~form)))

(defmacro shovem* [x form] 
  (if (seq? form) 
    (if (= 'quote (first form))
      `(-> ~x ~(second form))
      `(-> ~x ~form)) 
    `(shove ~x ~form)))

Now, shovem* has the semantics you are looking for

(def move '(count))

(shovem* [1 2 3] count) ;=> 3
(shovem* [1 2 3] (count)) ;=> 3
(shovem* [1 2 3] '(count)) ;=> 3
(shovem* [1 2 3] move) ;=> 3
(let [f count, d [1 2 3]] (shovem* d f)) ;=> 3

The problem (?) with the original macro

user=> (def move '(count))
user=> (defmacro shovem [data form] `(-> ~data ~form))

user=> (macroexpand-1 '(shovem [1 2 3] move))
(clojure.core/-> [1 2 3] move)

user=> (macroexpand-1 '(clojure.core/-> [1 2 3] move)) 
(move [1 2 3])

user=> (macroexpand-1 '(move [1 2 3]))
(move [1 2 3]) ; same, move is not a macro

That ends the macro expansion phase. Now (move [1 2 3]) is code. What happens when it is evaluated?

user=> (move [1 2 3])
ClassCastException clojure.lang.PersistentList cannot be cast to clojure.lang.IFn 

If the why isn't obvious, you need to reconsider the evaluation rules. The form (move [1 2 3]) is a list, and move is not special form or a macro. So this is considered a function invocation of move on its argument [1 2 3]. But what is move?

user=> (type move)
clojure.lang.PersistentList

user=> (ifn? move)
false

So, move is not a function and doesn't know how to act like one. It's just a list.

user=> (= move (list 'count))
true    

OTHER TIPS

In your code, shovem gets passed move the symbol, not the actual value of move, because it is a macro. So your call to shovem gets expanded to:

(-> [1 2 3] move)

-> is another macro that implicitly wraps move in a list, since it is a symbol, so this code is equivalent to:

(-> [1 2 3] (move))

That is why, after -> is fully expanded, it becomes

(move [1 2 3])

And move is a sequence, not a function, hence the java.lang.ClassCastException

I'm not sure your goal of making the macro work for all input is feasible, since being a macro that runs prior to any code execution, it doesn't know if the move symbol passed is something that should be evaluated (to get (count)) or just used literally. Generally macros know whether, or how many times, each argument should be evaluated, based solely on the form of the arguments passed to it, and not on their run time values.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top