Search code examples
macrosclojureland-of-lisp

Evaluating macro arguments in clojure


I'm trying to translate the following macro from land of lisp into clojure:

(defmacro tag (name atts &body body)
  `(progn (print-tag ',name
                     (list ,@(mapcar (lambda (x)
                                       `(cons ',(car x) ,(cdr x)))
                                     (pairs atts)))
                     nil)
          ,@body
          (print-tag ',name nil t)))

But I keep getting stuck with atts requiring 1 more level of evaluation. E.g. the following needs to evaluate t#:

(defmacro tag [tname atts & body]
  `(do (print-tag '~tname '[~@(map (fn [[h# t#]] [h# t#]) (pair atts))] nil)
     ~@body
     (print-tag '~tname nil true)))

As it produces stuff like:

(tag mytag [color 'blue size 'big])
<mytag color="(quote blue)" size="(quote big)"><\mytag>

Where I want the attribute to evaluated. If I use "(eval t#)" in the above I fall foul of problems like this:

(defn mytag [col] (tag mytag [colour col]))
java.lang.UnsupportedOperationException: Can't eval locals (NO_SOURCE_FILE:1)

Any suggestions?

Why does it seem like one less level of evaluation happens in Clojure?

Definitions of supporting functions:

;note doesn't handle nils because I'm dumb
(defn pair [init-lst]
      (loop [lst init-lst item nil pairs []]
    (if (empty? lst)
      pairs
      (if item
        (recur (rest lst) nil (conj pairs [item (first lst)]))
        (recur (rest lst) (first lst) pairs)))))

(defn print-tag [name alst closing]
      (print "<")
      (when closing
    (print "\\"))
      (print name)
      (doall
      (map (fn [[h t]]
           (printf " %s=\"%s\"" h t))
       alst))
      (print ">"))

(For some reason I didn't do the pair function in the same way as the book which means it doesn't handle nils correctly)


Solution

  • Your Clojure definition of tag quotes everything in the attribute map, while the common lisp version quotes only the names. That's the immediate source of your problems - if you just dropped the ' in front of your vector/map, and then fiddled with the map to quote the first element, you'd probably be fine.

    However, while porting may be a good exercise, this code is not written in The Clojure Way: the printing is a nasty ucky side effect that makes it hard to use print-tag to do anything meaningful; returning a string instead would be much nicer.

    (defmacro tag [name attrs & body]
      `(str "<" 
            (clojure.string/join " "
                                 ['~name
                                  ~@(for [[name val] (partition 2 attrs)]
                                      `(str '~name "=\"" ~val "\""))])
            ">"
            ~@body
            "</" '~name ">"))
    
    user> (tag head [foo (+ 1 2)] "TEST" (tag sample []))
    "<head foo=\"3\">TEST<sample></sample></head>"
    

    Of course, since order doesn't matter, using a map instead of a vector is nicer for the attributes. That would also mean you could drop the (partition 2...), since a sequential view of a map comes out as pairs already.

    And once we've gotten this far, it turns out that there are already plenty of ways to represent XML as Clojure data structures, so I would never use my above code in a real application. If you want to do XML for real, check out any of Hiccup, prxml, or data.xml.