Clojure可变参数宏对在&额外参数中收集的序列进行迭代

Clojure variadic macro iterating over sequences collected in & extra parameter

问题:当要传递的参数是序列,并且需要将所有变量作为序列序列处理时,如何处理宏中&之后的所有参数?包罗万象的变量中列出的是文字表达式。

这是一个宏,旨在大致表现Common Lisp的mapc,即做Clojure的map所做的事情,但仅出于副作用且没有延迟:

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

我已经意识到这不是编写domap的好方法-在这个问题上,我对此有很好的建议。但是,我仍然想知道一路上遇到的棘手的宏问题。

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

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

但是在这种情况下不起作用:

1
2
3
4
user=> (domap println (range 3))
range
3
nil

或者这个:

1
2
3
4
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)的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
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#等的各种组合。没有任何效果。尝试将其写为宏也许是一个错误,但是我仍然很好奇如何编写一个可变参宏,它需要像这样的复杂参数。


这是您的宏不起作用的原因:

'(~@colls)此表达式创建所有列的带引号的列表。例如如果将其传递给(range 3),则该表达式将变为'((range 3)),因此文字参数将是您的coll之一,从而阻止对(range 3)的求值当然不是您想要的。

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

您可以使用list来避免此问题:

1
2
3
4
5
6
7
8
(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

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

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

coll并不是我们唯一需要防止对其进行多次评估的事情。如果用户将(fn [& args] ...)之类的内容作为f传递,则该lambda也将在每个步骤中进行编译。

现在正是这种情况,您应该问自己为什么要编写宏。本质上,您的宏必须确保所有参数均已评估,而之前未进行任何形式的转换。评估免费提供函数,因此让我们将其编写为函数:

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

给定您要实现的目标,请注意已经有一个函数可以解决该问题,dorun仅实现一个seq但不保留head。例如:

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

也会做到这一点。

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

1
2
3
4
5
6
7
(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是执行Im试图执行的操作的自然方法,但我想指出,它并不总是最快的(请参阅我的问题所链接的问题)。如果确实想防止对此功能进行多次求值,是否可以在let绑定中添加f# ~f并在apply之后用f#替换~f来完成?
  • 是的,将f一次绑定到生成的符号将解决此问题。但是,即使在下面的讨论中,您链接到A. Webb的答案也正确地指出dorun的空间复杂度为O(1),因为只有头部保留在内存中。从宏生成循环可能运行得更快,但是每个步骤的开销应该非常小。与噪声史密斯基准相比,请基准(comp dorun map)
  • 我非常感谢Im所提供的帮助,并且我的问题中的宏表现出色,但是:反复被告知dorun + map是解决方案,它令人沮丧。这取决于。这是做我想要的事情的最简单的方法,但是....我一直以此为基准,我知道那并不总是最快的。 (comp dorun map):使用单个元素作为向量z-clj向量的索引,通过单个1000个元素的向量迭代54微秒。 Noisesmiths mapv定义:20-22毫秒,doseq:19毫秒,recur:35毫秒,简单dotimes:19毫秒。其他人更糟。
  • 校正:doseq需要12-13微秒。 (对于尚未阅读另一个问题的人(为什么要这么做?),是的,我正在使用Criterium。)
  • 这很可能是因为seq在输入序列上被调用。 doseq使用分块序列,这些分块序列将在向量上更快地工作。 doseq当然是遍历一个副作用序列的最惯用的方式。在向量上,它的表现应与向量上的dotimes几乎相同(查找索引),该向量应该最接近O(1)。我提供了dorun+map,因为您要求的是mapc等效项。 mapv创建一个结果序列,所以我永远不建议仅将其用于副作用。
  • 关于,它不是list周围的语法糖,而是直译为quote的文字。 quote是一种特殊形式,它告诉评估者"不要评估"。
  • 关于dorun + map:谢谢,我理解。反引号,好,知道了。 (我只是在(range 100000000)上尝试了doseqdorun+map的对决; doseq的速度大约是以前的两倍。我认为Clojure应该有一个块状的mapc,但我可能是唯一想到这一点的人!可能是Google小组的倡导者。我不了解如何重新利用Clojure源代码中的分块代码(不了解)。
  • f不在其原始代码中多次编译,仅多次评估。当然,那绝对是不好的,因为它可以来自某些昂贵的函数,但与多重编译有很大不同。
  • @amalloy:谢谢。您愿意详细说明原因吗?
  • 因为lambda只在编译时编译一次,而在运行时才编译一次。像(~f (~f (~f x#)))一样扩展的宏将编译f三次,因为它将扩展为三个不相关的lambda。但是他的帖子中的doseq仅包含一个lambda,因此仅被编译一次。
  • 不,以f形式传递到他的宏的表格?钪找运褂玫?code>dotimes生成的循环形式结束,然后在每个步骤中进行编译。
  • 11>
    传递给map的lambda也将在每个步骤中进行编译。
  • 11>
    不,那根本不是事情的运作方式。 dotimes扩展为仅包含f一次的形式,因此f仅被编译一次。传递给map的lambda也是如此。两者都经过了多次评估,但这仅仅是对new / malloc的调用,这比对编译器的调用便宜大约十亿倍。
  • 11>
    仅仅因为一次读取表单并不意味着它或它的一部分被编译一次。两个lambda在以loop形式结束之前都不会进行评估。 loop是一种特殊形式,因此,像宏一样,它们未经评估就被接收。 loop现在需要在每个步骤进行编译,因为它们可能包含例如循环变量。相反,传递给map的lambda将作??code>(map ...))形式的一部分进行评?ㄔ诿扛?code>loop?校U馐且蛭?code>map是函数调用。 map进程接收一个已编译的函数对象,可以在每个迭代中重复
  • ?PMXU11>
    简短地说:loop求值,fn编译。我最初以为您指的是一些分析器/优化,其中fn形式被散列,但我现在意识到这是不可能?ɑ蛘呤