views:

110

answers:

2

The following code executes as expected but gives a NullPointerException at the end. What am I doing wrong here?

(ns my-first-macro)

(defmacro exec-all [& commands]
  (map (fn [c] `(println "Code: " '~c "\t=>\tResult: " ~c)) commands))

(exec-all
  (cons 2 [4 5 6])
  ({:k 3 :m 8} :k)
  (conj [4 5 \d] \e \f))

; Output:
; Clojure 1.2.0-master-SNAPSHOT
; Code:  (cons 2 [4 5 6])   =>  Result:  (2 4 5 6)
; Code:  ({:k 3, :m 8} :k)  =>  Result:  3
; Code:  (conj [4 5 d] e f)     =>  Result:  [4 5 d e f]
; java.lang.NullPointerException (MyFirstMacro.clj:0)
; 1:1 user=> #<Namespace my-first-macro>
; 1:2 my-first-macro=> 

(For properly syntax highlighted code, go here.)

+10  A: 

Take a look at the expansion that is happening:

(macroexpand '(exec-all (cons 2 [4 5 6])))
=>
((clojure.core/println "Code: " (quote (cons 2 [4 5 6])) "\t=>\tResult: " (cons 2 [4 5 6])))

As you can see, there is an extra pair of parentheses around your expansion, which means that Clojure tries to execute the result of the println function, which is nil.

To fix this I'd suggest modifying the macro to include a "do" at the front, e.g.

(defmacro exec-all [& commands]
  (cons 'do (map (fn [c] `(println "Code: " '~c "\t=>\tResult: " ~c)) commands)))
mikera
+1 jejej, get use to those parenthesis :)
OscarRyz
+1, any alternative way to fix?
missingfaktor
Sure, you could rewrite it to expand to a `doseq` etc. But why? This is a perfectly reasonable solution and the change to your existing code is minimal; I'd say stick with it.
Michał Marczyk
@Michael: because I believe knowing alternative ways will aid in my Clojure learning.
missingfaktor
Another option I guess would be to change the output of each function to a string rather than a println and concatenate them all together. I think this is the natural "side effect free" way of doing this.
mikera
@Rahuλ G: Fair enough. I'll add another answer with a `doseq`-based version.
Michał Marczyk
...obviously after upvoting the best way of fixing the problem displayed in the answer above. :-)
Michał Marczyk
+5  A: 

Since the OP asked for other possible ways of writing this macro (see comments on the accepted answer), here goes:

(defmacro exec-all [& commands]
  `(doseq [c# ~(vec (map (fn [c]
                           `(fn [] (println "Code: " '~c "=> Result: " ~c)))
                         commands))]
     (c#)))

This expands to something like

(doseq [c [(fn []
             (println "Code: "      '(conj [2 3 4] 5)
                      "=> Result: " (conj [2 3 4] 5)))
           (fn []
             (println "Code: "      '(+ 1 2)
                      "=> Result: " (+ 1 2)))]]
  (c))

Note that the fn forms whose values will be bound to c are collected in a vector at macro-expansion time.

Needless to say, the original version is simpler, thus I think (do ...) is the perfect fix. :-)

Example interaction:

user=> (exec-all (conj [2 3 4] 5) (+ 1 2))                                                                                                    
Code:  (conj [2 3 4] 5) => Result:  [2 3 4 5]
Code:  (+ 1 2) => Result:  3
nil
Michał Marczyk
+1, thanks for the answer. :-)
missingfaktor