将向量作为绑定传递给for宏的问题

11

我有很多不确定数量的列表,我想使用for宏来处理它们。我想创建一个函数,将向量作为绑定传递,因为列表的数量是可变的。

如果我硬编码绑定,那么它按照我的预期工作:

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

当我尝试分别创建向量并将其用作绑定时,遇到了问题。在这里,我手动创建了绑定向量:

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

这看起来不错。

=> testvector
[A ("pink" "green") B ("dog" "cat")]
=> (class testvector)
clojure.lang.PersistentVector
然而,
=> (for testvector (str A "-" B))
#<CompilerException java.lang.IllegalArgumentException: for requires a vector for its binding (NO_SOURCE_FILE:36)>

我不明白为什么在for循环中使用testvector作为绑定变量时它不被视为向量。我尝试着抓住了一些东西,将testvector放在方括号中,这使得for宏看到了一个向量,但是现在我有了一个只有一个元素的向量(即一个向量内部还有一个向量),这是不起作用的,因为绑定需要成对的名称和集合。

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

欢迎提供关于如何动态地将向量作为绑定传递给for的任何建议。


1
(vec (list ...)) 可以简写为 (vector ...) - kotarak
4个回答

6
关键在于 for 是一个宏。在宏展开时,testvector 是一个符号。它会在评估时评估为一个向量,但从 for 宏的角度来看,它不是一个向量。
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

如果你查看for宏的源代码(在core.clj中),你会发现for使用了一个未引用的vector?调用,就像上面例子中的tst一样。


1
对于那些仍在学习Clojure的人来说,实际使其工作的下一步是什么?我尝试了(defmacro combo [v] `(for ~v [A B])),但它不起作用,并显示了与需要一个向量的_for_相同的错误消息。 - Jonathan Benn
1
@JonathanBenn 即使您将_for_包装在宏中,它本身仍然是一个宏,同样的限制也适用。这个答案并不是一个解决方案,只是解释为什么它不起作用。目前我无法想出一个聪明的宏时间解决方案,但我认为您可以通过递归函数解决OP的测试问题。 - G__
谢谢。我一直在苦苦寻找解决方案,但我的Clojure技能还不够高级。我会尝试您的递归函数建议。 - Jonathan Benn

1
你可以尝试强制计算绑定向量。不要试图定义一个包装for宏的宏,而是将其包装在一个函数中,例如:
(defn for-fn [bindings expr]
  (eval `(for ~bindings ~expr))) 

接着,您可以通过几个附加约束条件构建绑定向量,因为绑定向量中的所有S表达式都需要有效且包含动词作为第一个元素。

(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)) 

而且根据您的示例:

(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")

注意:由于 for-fn 是函数,您需要引用表达式 (str A "-" B),以防止在 A 和 B 绑定之前进行早期评估。

0

这里有一个最后的手段。请注意,无论何时您看到read-string,那都是代码的代表危险!(由于安全风险以及关于代码行为缺乏编译时一致性保证)

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

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

(def testvector-vec (vec (list 'A (vec list1) 'B (vec list2))))

(def for-string (str "(for " testvector-vec "(str A \"-\" B))"))

(eval (read-string for-string))
> ("pink-dog" "pink-cat" "green-dog" "green-cat")

1
不要使用字符串来操作Lisp中的任何代码。这是一个Lisp,所以你可以直接使用(eval \(for ~testvector-vec (str ~'A "-" ~'B)))`。 - Johannes Kuhn

0

虽然这不是解决你问题的方法,但需要注意的是,你所做的事情可以更容易地使用map而不是for等方式实现。

user=> (def list1 '("pink" "green"))
#'user/list1
user=> (def list2 '("dog" "cat"))
#'user/list2
user=> (map #(str %1 "-" %2) list1 list2)
("pink-dog" "green-cat")
user=> 

在学习和实验时,另一个有用的技巧是使用关键字而不是字符串。这可以减少打字,即无需将值放在引号中,并且有时可以更容易地识别错误。您可以使用 (def list1 '(:pink :green)) 代替 (def list1 '("pink" "green"))。更好的方法是尝试使用向量而不是列表,然后您就不必引用它(再节省一次击键)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接