【问题标题】:Clojure variadic macro iterating over sequences collected in & extra parameterClojure 可变参数宏迭代在 & 额外参数中收集的序列
【发布时间】:2014-02-04 03:22:53
【问题描述】:

问题:宏中&之后的catch-all参数,当传递的参数是序列时,catch-all变量需要作为序列的序列处理?包罗万象的变量中列出的是文字表达式。

这是一个宏,旨在大致表现 Common Lisp 的 mapc,即做 Clojure 的 map 所做的,但用于副作用,并且没有懒惰:

(defmacro domap [f & colls] 
      `(dotimes [i# (apply min (map count '(~@colls)))]
         (apply ~f (map #(nth % i#) '(~@colls)))))

我开始意识到这不是写domap 的好方法——我在this 问题中得到了很好的建议。但是,我仍然想知道我在此过程中遇到的棘手的宏观问题。

如果集合作为文字传递,则此方法有效:

user=> (domap println [0 1 2])
0
1
2
nil

但在像这样的其他情况下不起作用:

user=> (domap println (range 3))
range
3
nil

或者这个:

user=> (def nums [0 1 2])
#'user/nums
user=> (domap println nums)
UnsupportedOperationException count not supported on this type: Symbol clojure.lang.RT.countFro (RT.java:556)

问题在于colls 中的文字表达式。这就是为什么宏 domap 在传递整数序列时有效,但在其他情况下无效。注意'(nums)的实例:

user=> (pprint (macroexpand-1 '(domap println nums)))
(clojure.core/dotimes
 [i__199__auto__
  (clojure.core/apply
   clojure.core/min
   (clojure.core/map clojure.core/count '(nums)))]
 (clojure.core/apply
  println
  (clojure.core/map
   (fn*
    [p1__198__200__auto__]
    (clojure.core/nth p1__198__200__auto__ i__199__auto__))
   '(nums))))

我尝试了~~@'letvar# 等的各种组合。没有任何效果。尝试将其编写为宏可能是一个错误,但我仍然很好奇如何编写一个接受此类复杂参数的可变参数宏。

【问题讨论】:

    标签: macros clojure


    【解决方案1】:

    这就是你的宏不起作用的原因:

    '(~@colls) 这个表达式创建一个包含所有 coll 的引用列表。例如。如果你传递(range 3),这个表达式就变成'((range 3)),所以literal 参数将是你的colls 之一,阻止(range 3) 的评估肯定不是你想要的。

    现在如果你不在宏内引用(~@colls),当然它们会变成像((range 3))这样的字面函数调用,这会使编译器在宏扩展时间之后抛出(它会尝试评估((0 1 2)))。

    你可以使用list来避免这个问题:

    (defmacro domap [f & colls]
      `(dotimes [i# (apply min (map count (list ~@colls)))]
         (apply ~f (map #(nth % i#) (list ~@colls)))))
    
    => (domap println (range 3))
    0
    1
    2
    

    但是这里有一件很糟糕的事情:在宏内部,整个列表被创建两次。以下是我们可以避免这种情况的方法:

    (defmacro domap [f & colls]
      `(let [colls# (list ~@colls)]
         (dotimes [i# (apply min (map count colls#))]
           (apply ~f (map #(nth % i#) colls#)))))
    

    colls 并不是我们唯一需要防止被多次评估的东西。如果用户将(fn [& args] ...) 之类的内容传递为f,则该lambda 也会在每一步中编译。

    现在这正是您应该问自己为什么要编写宏的场景。从本质上讲,您的宏必须确保所有参数都经过评估,而之前没有以任何方式转换它们。评估是免费提供的,所以我们把它写成一个函数:

    (defn domap [f & colls]
      (dotimes [i (apply min (map count colls))]
        (apply f (map #(nth % i) colls)))) 
    

    考虑到您想要实现的目标,请注意已经有一个函数可以解决这个问题,dorun 它只是实现了一个 seq,但不保留 head。例如:

    `(dorun (map println (range 3)))
    

    也可以解决问题。

    现在您已经有了dorunmap,您可以简单地使用comp 组合它们来实现您的目标:

    (def domap (comp dorun map))
    
    => (domap println (range 3) (range 10) (range 3))
    
    0 0 0
    1 1 1
    2 2 2
    

    【讨论】:

    • 谢谢——正是我想要的。我没想到需要对list 进行显式调用——我认为反引号是(list ...) 的语法糖。感谢关于函数的多重评估的观点——一直想知道这一点。尽管dorun + map 是做我想做的事情的自然方式,但我想指出它并不总是最快的(请参阅我的问题链接的问题)。如果确实想防止对与此类似的函数进行多次评估,是否会在let 绑定中添加f# ~f,并在apply 之后将~f 替换为f# 完成此操作?
    • 是的,将 f 绑定到生成的符号一次可以解决问题。然而,即使在下面的讨论中,您链接到 A. Webb 的答案也正确地指出dorun 在空间复杂度上的执行时间为 O(1),因为只有头部保存在内存中。从宏生成循环可能会运行得更快,但每一步的开销应该非常非常少。与noisesmiths基准相比,请基准(comp dorun map)
    • 我非常感谢我得到的帮助,我的问题中的宏执行得非常糟糕,但是:被反复告知 dorun + mapthe 解决方案。这取决于。这是做我想做的最简单的方法,但是....我一直在做基准测试,我知道这并不总是最快的。 (comp dorun map):54 微秒迭代单个 1000 元素向量,使用每个元素作为 vectorz-clj 向量的索引。 Noisesmith 的 mapv 定义:20-22 微秒,doseq:19 微秒,recur:35 微秒,简单的dotimes:19 微秒。其他的更糟。
    • 更正:doseq 需要 12-13 微秒。 (对于尚未阅读其他问题的任何人(您为什么要阅读?),是的,我正在使用 Criterium。)
    • 这很可能是因为在输入序列上调用了seqdoseq 使用分块序列,这将在向量上更快地工作。 doseq 无疑是遍历一个序列以获取副作用的最惯用的方式。在向量上,它应该与dotimes 在向量上的执行几乎相同(查找索引),这应该是最接近 O(1) 的值。我提供了dorun+map,因为您要求提供mapc 等价物。 mapv 创建结果序列,因此我绝不建议仅将其用于副作用。
    猜你喜欢
    • 1970-01-01
    • 2010-12-24
    • 1970-01-01
    • 2020-02-06
    • 2022-11-13
    • 1970-01-01
    • 1970-01-01
    • 2016-09-20
    相关资源
    最近更新 更多