Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problem with passing a vector as a binding to the for macro

I have an arbitrary number of lists which I would like to process using the for macro. I want to create a function that passes a vector as the binding since the number of lists varies.

If I hard code the binding, it works as I expect:

=> (def list1 '("pink" "green"))
=> (def list2 '("dog" "cat"))
=> (for [A list1 B list2] (str A "-" B))
("pink-dog" "pink-cat" "green-dog" "green-cat")

When I try to create a vector separately and use this as the binding I hit problems. Here I manually create the bindings vector:

=> (def testvector (vec (list 'A list1 'B list2)))

this seems fine:

=> testvector
[A ("pink" "green") B ("dog" "cat")]
=> (class testvector)
clojure.lang.PersistentVector

However,

=> (for testvector (str A "-" B))
#<CompilerException java.lang.IllegalArgumentException: for requires a vector for its binding (NO_SOURCE_FILE:36)>

I don't understand why testvector isn't considered a vector when used as the binding in for. Grasping at straws, I put testvector in square brackets which keeps the for macro happy (it sees a vector) but now I have a vector with one element (i.e. a vector within a vector) and this doesn't work because the binding needs to be pairs of name and collection.

=> (for [testvector] (str A "-" B))
#<CompilerException java.lang.IllegalArgumentException: for requires an even number of forms in binding vector (NO_SOURCE_FILE:37)>

Any suggestions on how to dynamically pass a vector as a binding to for would be appreciated.

like image 819
Teflon Mac Avatar asked Jul 15 '10 00:07

Teflon Mac


2 Answers

The key is that for is a macro. At macro-expansion time, testvector is a symbol. It will evaluate to a vector at evaluation time, but it's not a vector from the perspective of the for macro.

user=> (defmacro tst [v] (vector? v))
#'user/tst
user=> (tst testvector)
false
user=> (vector? testvector)
true
user=> (defmacro tst2 [v] `(vector? ~v))
#'user/tst2
user=> (tst2 testvector)
true

If you check the source for the for macro (in core.clj), you'll see that for uses an unquoted vector? call, just like tst in the example above.

like image 95
G__ Avatar answered Oct 16 '22 12:10

G__


You can try to force the evaluation of the binding vector. Instead of trying to define a macro that will wrap the for macro, wrap it in a function, e.g.

(defn for-fn [bindings expr]
  (eval `(for ~bindings ~expr))) 

Then you can actually build a binding vector with a few additional constraints since all s-expressions inside the binding vector need to be valid and contain a verb as the first element.

(let [bindings '[a (list 1 2) b (list 3 4) c (range 10 12)
                 :when (> (+ a b c) 15)]
      expr '(str a "-" b "-" c)]
  (for-fn bindings expr)) 

And with your example :

(def list1 '("pink" "green"))
(def list2 '("dog" "cat"))
(def testvector (vector 'A (cons 'list  list1) 'B (cons 'list list2)))

(for-fn testvector '(str A "-" B))
=> ("pink-dog" "pink-cat" "green-dog" "green-cat")

Note : since for-fn is function, you need to quote the expression (str A "-" B) to prevent an early evaluation (before A & B are bound).

like image 20
T.Gounelle Avatar answered Oct 16 '22 12:10

T.Gounelle