为什么在Clojure中应该使用'apply'?

51

以下是 Rich Hickey 在博客中的一段话,但我不明白使用 apply 的动机。请帮忙解释。

Clojure 和 CL 之间的一个重要区别是 Clojure 是 Lisp-1,因此不需要 funcall,而只有在将函数应用于运行时定义的参数集合时才使用 apply。因此,(apply f [i]) 可以写作 (f i)。

此外,他所说的 "Clojure 是 Lisp-1" 和不需要 funcall 是什么意思?我从未在 CL 中编程过。

谢谢



这里是一个包含apply函数源代码的gist链接:https://gist.github.com/purplejacket/36de7061061ec9c49734 - Purplejacket
6个回答

58

如果要传递给函数的参数数量在编译时不确定(对不起,我不太熟悉Clojure语法,转而使用Scheme),则可以使用apply

(define (call-other-1 func arg) (func arg))
(define (call-other-2 func arg1 arg2) (func arg1 arg2))
只要在编译时知道参数的数量,就可以像上面的示例一样直接传递它们。但是,如果在编译时不知道参数的数量,则无法这样做(好吧,你可以尝试类似以下的东西):
(define (call-other-n func . args)
  (case (length args)
    ((0) (other))
    ((1) (other (car args)))
    ((2) (other (car args) (cadr args)))
    ...))

但很快这就变成了一个噩梦。这时,apply 就出现了:

(define (call-other-n func . args)
  (apply other args))

它将获取作为最后一个参数给出的列表中包含的任意数量的参数,并使用这些值调用作为第一个参数传递给 apply 的函数。


43

Lisp-1和Lisp-2这两个术语指的是函数是否与变量在同一个命名空间中。

在Lisp-2(也就是2个命名空间)中,表单中的第一个项目将被作为函数名进行评估,即使它实际上是具有函数值的变量的名称。因此,如果要调用变量函数,您必须将变量传递给另一个函数。

在Lisp-1中,例如Scheme和Clojure,评估为函数的变量可以放在初始位置,因此您无需使用apply来将其评估为函数。


38

apply 基本上是将一个序列解包,并将其作为单独的参数传递给函数。

以下是一个示例:

(apply + [1 2 3 4 5])

这将返回15。它基本上展开为(+ 1 2 3 4 5),而不是(+ [1 2 3 4 5])


9
又一个例子: (apply + 1 2 [3 4 5]) => 15 意思是将函数加号(+)应用于列表 [3 4 5] 中的所有元素,并将结果与 1 和 2 相加,得到最终答案 15。 - Isaiah
...但是这个失败了(apply + 1 2 [3 4 5]),这让我很意外。有什么想法吗?(我对Clojure完全是新手) - ianjs
嗯...我刚刚在REPL中输入了那个,你说得对,它可以正常工作。可能是我两年前设置的环境有些问题。 - ianjs

9

使用 apply 将一个作用于多个参数的函数转化为作用于单一参数序列的函数。你也可以在序列之前插入参数。例如,map 可以作用于多个序列。这个示例(来自ClojureDocs)使用 map 来转置矩阵。

user=> (apply map vector [[:a :b] [:c :d]])
([:a :c] [:b :d])

这里插入的唯一参数是vector。因此,apply扩展为

user=> (map vector [:a :b] [:c :d])

太可爱了!

PS: 如果要返回一个向量的向量而不是一个向量序列,请将整个内容包装在vec中:

user=> (vec (apply map vector [[:a :b] [:c :d]]))

在这个语境下,vec 可以被定义为(partial apply vector),但事实上它并没有被定义成这样。

关于Lisp-1和Lisp-2:数字1和2表示在给定上下文中名称可以表示的事物数量。在Lisp-2中,你可以有两个不同的事物(一个函数和一个变量)具有相同的名称。因此,在任何一个可能有效的地方,您需要用某些东西装饰您的程序来指示您的意思。幸运的是,Clojure(或Scheme…)允许名称仅表示一个事物,因此无需进行任何装饰。


4
通常,应用类型操作的常规模式是将运行时提供的函数与一组参数结合使用。我对Clojure的了解还不够,无法确定在该特定语言中使用apply是否绝对必要。

1

apply在协议中非常有用,特别是与线程宏一起使用。我刚刚发现了这一点。由于您无法在编译时使用&宏来扩展接口参数, 因此可以使用大小不可预测的向量。

例如,我将其用作记录和某个特定xml文件本身之间的接口的一部分,记录包含有关该文件的一些元数据。

(query-tree [this forms]
  (apply xml-> (text-id-to-tree this) forms)))

text-id-to-tree 是这个特定记录的另一种方法,它将文件解析为 XML 拉链。在另一个文件中,我使用特定查询扩展了协议,实现了 query-tree,指定要通过 xml-> 宏线程化的命令链:

(tags-with-attrs [this]
  (query-tree this [zf/descendants zip/node (fn [node] [(map #(% node) [:tag :attrs])])])

(注意:这个查询本身会返回很多没有属性的标签的“nil”结果。使用过滤和缩减功能可以得到一个干净的唯一值列表)。

顺便提一下,zf是指clojure.contrib.zip-filter,zip是指clojure.zip。xml->宏来自clojure.contrib.zip-filter.xml库,我使用了:use命令。


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