【问题标题】:Use of count function in a clojure macro gives exception在 clojure 宏中使用 count 函数会产生异常
【发布时间】:2013-01-21 06:47:13
【问题描述】:

我有这个创建符号向量的函数:

(defn makevars [n] 
"Gives a list of 'n' unique symbols" 
(let [vc (repeatedly n #(gensym ))] vc)) 

这是有问题的宏:

(defmacro make-strings [syms]
;eg given 2 strings ["abc" "ab"] => ("aa" "ab" "ba" "bb" "ca" "cb")  
(let [nl (count syms) vs (makevars nl) forargs (vec (interleave vs syms))]  
`(for ~forargs (str ~@vs))))

当使用(make-strings ["abc" "ab"]) 执行宏时,会出现所需的输出。但是,如果宏以(make-strings sy) 运行,其中sy 定义为["abc" "ab"],则会发生此错误:

UnsupportedOperationException count not supported on this type: Symbol  clojure.lang.RT.countFrom (RT.java:545)

我做错了什么,如何解决?作为宏观知识的新手,我希望我的理解有些不对劲。

【问题讨论】:

  • 你需要一个函数而不是宏。或者使用 eval : (eval `(make-strings ~sy))
  • 现在在宏 args 中传递硬数据和 var 之间的区别是有意义的。谢谢。去宏根不是我的第一选择,但它给了我一些成功,但非常有限。

标签: macros clojure


【解决方案1】:

首先,我强烈建议您在此处删除宏,并使用普通函数实现您想要实现的逻辑:宏仅在您想要创建一些方便的自定义语法时有用,并且您只想计算两组字符的笛卡尔积。最简单的方法是使用 contrib lib math.combinatorics:

user=> (def sy ["abc" "ab"])
user=> (map #(apply str %) (apply cartesian-product sy))
("aa" "ab" "ba" "bb" "ca" "cb")

也就是说,您遇到的问题是宏在编译时执行,因此会接收您作为参数传递的未评估形式。这意味着当您调用时:

(make-strings sy)

不是传递sy所引用的向量,而是将符号sy赋予make-strings宏,以便调用

(count sy)

然后将触发上述异常。由于宏由于其编译时间性质而无法评估其输入,因此您永远无法从宏中获取符号背后的值:对此类符号的所有操作都必须以宏本身的返回形式完成,即然后在运行时执行。

当您希望能够传递文字或符号时,上述内容使您制作 for 绑定的策略不可行。

【讨论】:

  • 感谢您提出这个想法。我曾考虑过笛卡尔积,但现在愚蠢地拒绝了它,因为我认为它只会处理 2 个字符串,而不是使用“应用”的任何 #。我会记得更多地测试内置功能。我对clojure印象最深刻。我还用 Java 编写了这个函数,大约 18 行。
  • 我写 clojure 已经三年多了,并且我不断(重新)发现标准库或 contrib 库中的 fn 有助于改进我的代码。如果我在你的鞋子里,我会习惯这种感觉;-)
【解决方案2】:

我认为 skuro 很好地回答了问题所在和原因,还建议了一个非常有用的库,您可以使用它来解决您的原始目标。 “那我为什么要读这个?”我听到你问。我只是觉得分享一下如何只用几行代码就可以将函数应用于事物的笛卡尔积会很有趣——而不会侵入可怕的宏观领域。

当我说泛化时,我的意思是目标是能够做类似的事情:

user> (permute str ["abc" "ab"])
=> ("aa" "ab" "ba" "bb" "ca" "cb")

如果我们开发这个迄今为止不存在的置换函数,好消息是我们可以使用相同的函数来执行以下操作:

user> (permute + [[1 2 3] [10 20 30]])
=> (11 21 31 12 22 32 13 23 33)

这是一个玩具示例,但希望能传达您通过这种方式进行概括所获得的灵活性。

嗯,这是我想出的一个非常简洁的方法:

(defn permute [f [coll & remaining]]
  (if (nil? remaining)
    (map f coll)
    (mapcat #(permute (partial f %) remaining) coll)))

我开始的核心想法是使用mapmapcat 进行迭代,因为要组合不同的字符串就可以进行尽可能多的迭代。我从你的例子开始,写了一个“冗长”的非通用解决方案:

user> (mapcat (fn [i] (map (partial str i) "ab")) "abc")
=> ("aa" "ab" "ba" "bb" "ca" "cb")

这个mapcats 是一个功能下降"abc"。具体来说,它 mapcats 一个函数向下 "abc" 那个 maps string-ing 一起使用的单个元素来自 "abc" 此时 (i),每个元素来自 "ab" .

为了让我理解如何将其概括为一个函数,我必须“更深一层”,并尝试使用第三个字符串。

user> (mapcat (fn [i] (mapcat (fn [j] (map (partial str i j) "def")) "ab")) "abc")
=> ("aad" "aae" "aaf" "abd" "abe" "abf" "bad" "bae" "baf" "bbd" "bbe" "bbf" "cad" "cae" "caf" "cbd" "cbe" "cbf")

mapcats 是一个函数 mapcats 是 maps string-ing 以组合方式将元素组合在一起的函数。呸。现在我开始了解如何进行概括。最里面的表达式总是maping 某种部分的str 函数,在要重新组合的字符串列表中的最后一个字符串。外部表达式只是mapcats 与连续更靠前的字符串,直到最外面的地方使用字符串列表中的第一个字符串重新组合。

我想从这里我注意到我不需要一次定义整个部分 str 函数,而是可以“构建”它,因为我递归调用 permute

希望我现在已经给出了足够的上下文来解释该函数的工作原理。 permute 的最后一次迭代发生在没有剩余 colls 时(即 (nil? remaining) 返回 true)。它只是 maps 在最后一个 coll 中给出的任何功能。

当还有剩余的 coll 时,它 mapcats 一个 permute 变体向下当前 coll。这个置换变体使用带有匿名参数的f 的部分函数,​​并且是permuteing 剩余的colls。通过这样做,它将逐步构建一个部分函数,​​一旦它到达 colls 列表的末尾,最终将被调用。然后,在我的脑海中,我想象它向后追溯,调用嵌套的mapcats,直到它最终解开并产生重新组合的 colls。

我想这个函数虽然简洁,但可能不是最佳的。坦率地说,我没有太多的 CS 背景,但从我读到的有关 Clojure 的内容来看,使用 loop/recur 而不是自递归调用往往更“有效”。如果优化对您很重要,我想重新设计函数以使用 loop/recur 会相当简单。

【讨论】:

  • 好主意。在接下来的几个月里,我可能需要更多类似的函数,所以假设我的 clojure 技能有所提高,泛化是很有意义的。
【解决方案3】:

不评估宏参数。当您将 ["abc" "ab"] 作为参数提供给宏时,它会将其作为字符串向量,因为字符串是自评估对象。但是当您发送sy 宏时,只会得到一个符号'sy。这就是发生错误的原因。

【讨论】:

    猜你喜欢
    • 2011-12-12
    • 1970-01-01
    • 1970-01-01
    • 2016-09-09
    • 1970-01-01
    • 2011-05-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多