Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to expand a sequence (var-args) into distinct items

Tags:

clojure

I want to send var-args of a function to a macro, still as var-args. Here is my code:

(defmacro test-macro
 [& args]
 `(println (str "count=" ~(count args) "; args=" ~@args)))

(defn test-fn-calling-macro
 [& args]
 (test-macro args))

The output of (test-macro "a" "b" "c") is what I want: count=3; args=abc

The output of (test-fn-calling-macro "a" "b" "c") is : count=1; args=("a" "b" "c") because args is sent as a single argument to the macro. How can I expand this args in my function in order to call the macro with the 3 arguments?

I guess I'm just missing a simple core function but I'm not able to find it. Thanks


EDIT 2 - My "real" code, shown in EDIT section below is not a valid situation to use this technique.

As pointed out by @Brian, the macro xml-to-cass can be replaced with a function like this:

(defn xml-to-cass
  [zipper table key attr & path]
  (doseq [v (apply zf/xml-> zipper path)] (cass/set-attr! table key attr v)))

EDIT - the following section goes beyond my original question but any insight is welcome

The code above is just the most simple I could come with to pinpoint my problem. My real code deals with clj-cassandra and zip-filter. It may also look over-engineering but it's just a toy project and I'm trying to learn the language at the same time.

I want to parse some XML found on mlb.com and insert values found into a cassandra database. Here is my code and the thinking behind it.

Step 1 - Function which works fine but contains code duplication

(ns stats.importer
  (:require
    [clojure.xml :as xml]
    [clojure.zip :as zip]
    [clojure.contrib.zip-filter.xml :as zf]
    [cassandra.client :as cass]))

(def root-url "http://gd2.mlb.com/components/game/mlb/year_2010/month_05/day_01/")

(def games-table (cass/mk-cf-spec "localhost" 9160 "mlb-stats" "games"))

(defn import-game-xml-1
  "Import the content of xml into cassandra"
  [game-dir]
  (let [url (str root-url game-dir "game.xml")
        zipper (zip/xml-zip (xml/parse url))
        game-id (.substring game-dir 4 (- (.length game-dir) 1))]
    (doseq [v (zf/xml-> zipper (zf/attr :type))] (cass/set-attr! games-table game-id :type v))
    (doseq [v (zf/xml-> zipper (zf/attr :local_game_time))] (cass/set-attr! games-table game-id :local_game_time v))
    (doseq [v (zf/xml-> zipper :team [(zf/attr= :type "home")] (zf/attr :name_full))] (cass/set-attr! games-table game-id :home_team v))))

The parameter to import-game-xml-1 can be for example "gid_2010_05_01_colmlb_sfnmlb_1/". I remove the "gid_" and the trailing slash to make it the key of the ColumnFamily games in my database.

I found that the 3 doseq were a lot of duplication (and there should be more than 3 in the final version). So code templating using a macro seemed appropriate here (correct me if I'm wrong).

Step 2 - Introducing a macro for code templating (still works)

(defmacro xml-to-cass
  [zipper table key attr & path]
  `(doseq [v# (zf/xml-> ~zipper ~@path)] (cass/set-attr! ~table ~key ~attr v#)))

(defn import-game-xml-2
  "Import the content of xml into cassandra"
  [game-dir]
  (let [url (str root-url game-dir "game.xml")
        zipper (zip/xml-zip (xml/parse url))
        game-id (.substring game-dir 4 (- (.length game-dir) 1))]
    (xml-to-cass zipper games-table game-id :type (zf/attr :type))
    (xml-to-cass zipper games-table game-id :local_game_time (zf/attr :local_game_time))
    (xml-to-cass zipper games-table game-id :home_team :team [(zf/attr= :type "home")] (zf/attr :name_full))))

I believe that's an improvement but I still see some duplication in always reusing the same 3 parameters in my calls to xml-to-cass. That's were I introduced an intermediate function to take care of those.

Step 3 - Adding a function to call the macro (the problem is here)

(defn import-game-xml-3
  "Import the content of xml into cassandra"
  [game-dir]
  (let [url (str root-url game-dir "game.xml")
        zipper (zip/xml-zip (xml/parse url))
        game-id (.substring game-dir 4 (- (.length game-dir) 1))
        save-game-attr (fn[key path] (xml-to-cass zipper games-table game-id key path))]
    (save-game-attr :type (zf/attr :type)) ; works well because path has only one element
    (save-game-attr :local_game_time (zf/attr :local_game_time))
    (save-game-attr :home :team [(zf/attr= :type "home"] (zf/attr :name_full))))) ; FIXME this final line doesn't work
like image 460
Damien Avatar asked Jan 13 '11 00:01

Damien


3 Answers

Here's a some simple code which may be illuminating.

Macros are about code generation. If you want that to happen at runtime, for some reason, then you have to build and evaluate the code at runtime. This can be a powerful technique.

(defmacro test-macro
 [& args]
 `(println (str "count=" ~(count args) "; args=" ~@args)))

(defn test-fn-calling-macro
 [& args]
 (test-macro args))

(defn test-fn-expanding-macro-at-runtime
  [& args]
  (eval (cons `test-macro args)))

(defmacro test-macro-expanding-macro-at-compile-time
  [& args]
  (cons `test-macro args))

;; using the splicing notation

(defmacro test-macro-expanding-macro-at-compile-time-2
  [& args]
  `(test-macro ~@args))

(defn test-fn-expanding-macro-at-runtime-2
  [& args]
  (eval `(test-macro ~@args)))



(test-macro "a" "b" "c") ;; count=3; args=abc nil
(test-fn-calling-macro "a" "b" "c") ;; count=1; args=("a" "b" "c") nil

(test-fn-expanding-macro-at-runtime "a" "b" "c") ; count=3; args=abc nil
(test-macro-expanding-macro-at-compile-time "a" "b" "c") ; count=3; args=abc nil
(test-macro-expanding-macro-at-compile-time-2 "a" "b" "c") ; count=3; args=abc nil
(test-fn-expanding-macro-at-runtime "a" "b" "c") ; count=3; args=abc nil

If contemplation of the above doesn't prove enlightening, might I suggest a couple of my own blog articles?

In this one I go through macros from scratch, and how clojure's work in particular:

http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-i-getting.html

And in this one I show why run-time code generation might be useful:

http://www.learningclojure.com/2010/09/clojure-faster-than-machine-code.html

like image 134
John Lawrence Aspden Avatar answered Nov 15 '22 07:11

John Lawrence Aspden


The typical way to use a collection as individual arguments to a function is to use (apply function my-list-o-args)

(defn test-not-a-macro [& args]
    (print args))

(defn calls-the-not-a-macro [& args]
   (apply test-not-a-macro args))

though you wont be able to use apply because test-macro is a macro. to solve this problem you will need to wrap test macro in a function call so you can apply on it.

(defmacro test-macro [& args]
    `(println ~@args))

(defn calls-test-macro [& args]
   (eval (concat '(test-macro) (args)))) ;you almost never need eval.

(defn calls-calls-test-macro [& args]
   (calls-test-macro args))

This is actually a really good example of one of the ways macros are hard to compose. (some would say they cant be composed cleanly, though i think thats an exageration)

like image 38
Arthur Ulfeldt Avatar answered Nov 15 '22 08:11

Arthur Ulfeldt


Macros are not magic. They are a mechanism to convert code at compile-time to equivalent code; they are not used at run-time. The pain you are feeling is because you are trying to do something you should not be trying to do.

I don't know the library in question, but if cass/set-attr! is a function, I see no reason why the macro you defined has to be a macro; it could be a function instead. You can do what you want to do if you can rewrite your macro as a function instead.

like image 40
Brian Avatar answered Nov 15 '22 09:11

Brian