Clojure是否可以完全实现动态性?

10
在Clojure 1.1中,所有调用都是动态的,这意味着您可以在REPL中重新定义一个函数,并且它将自动包含在运行中的程序中。这对于像dotrace这样的东西也很不错。
在Clojure 1.2中,许多调用似乎是静态链接的,如果我想替换一个函数,有时候我必须找到所有调用它的地方并在它们前面放置#'。
更糟糕的是,我无法预测何时需要这样做。
是否可以回到旧的默认动态链接方式?也许如果你需要额外的速度优势,你可以为生产应用程序切换回来,但是在开发过程中,我更喜欢1.1的行为。
我希望有一种编译器选项,类似于*warn-on-reflection*。
编辑:
我对正在发生的事情感到困惑。更具体地说,这里有两个函数。我喜欢第二个的行为。如何使第一个函数的行为像第二个函数,因为我认为它在1.1中就是这样做的?
user> (clojure-version)
"1.2.0"

user> (defn factorial[n] (if (< n 2) n (* n (factorial (dec n)))))
#'user/factorial

user> (require 'clojure.contrib.trace)
user> (clojure.contrib.trace/dotrace (factorial) (factorial 10))
TRACE t1670: (factorial 10)
TRACE t1670: => 3628800

user> (defn factorial[n] (if (< n 2) n (* n (#'factorial (dec n)))))
#'user/factorial
user> (clojure.contrib.trace/dotrace (factorial) (factorial 10))
TRACE t1681: (factorial 10)
TRACE t1682: |    (factorial 9)
TRACE t1683: |    |    (factorial 8)
TRACE t1684: |    |    |    (factorial 7)
TRACE t1685: |    |    |    |    (factorial 6)
TRACE t1686: |    |    |    |    |    (factorial 5)
TRACE t1687: |    |    |    |    |    |    (factorial 4)
TRACE t1688: |    |    |    |    |    |    |    (factorial 3)
TRACE t1689: |    |    |    |    |    |    |    |    (factorial 2)
TRACE t1690: |    |    |    |    |    |    |    |    |    (factorial 1)
TRACE t1690: |    |    |    |    |    |    |    |    |    => 1
TRACE t1689: |    |    |    |    |    |    |    |    => 2
TRACE t1688: |    |    |    |    |    |    |    => 6
TRACE t1687: |    |    |    |    |    |    => 24
TRACE t1686: |    |    |    |    |    => 120
TRACE t1685: |    |    |    |    => 720
TRACE t1684: |    |    |    => 5040
TRACE t1683: |    |    => 40320
TRACE t1682: |    => 362880
TRACE t1681: => 3628800
3628800

编辑(整个问题和标题更改):

Joost在下面指出,实际上发生的是阶乘中的自我调用被优化掉了。我不明白为什么会这样做,因为你不能进行那么多递归的自我调用而不会导致堆栈溢出,但这可以解释观察到的行为。也许这与匿名自我调用有关。

我提出问题最初的原因是我试图编写http://www.learningclojure.com/2011/03/hello-web-dynamic-compojure-web.html,并且我对我必须输入#'的地方感到恼火,以获得我期望的行为。那个点追踪和其他一些东西使我想到了通用动态行为已经消失了,并且在一些地方工作的即时重新定义必须使用一些巧妙的技巧来完成。

回想起来,这似乎是一个奇怪的结论,但现在我只是困惑了(这还好!)。有没有关于所有这些的参考资料?我很想拥有一个通用的理论,了解何时会起作用,何时不会。

4个回答

11

在Clojure中,一切都是完全动态的,但是你必须注意何时使用Var,何时使用该Var当前的Function值。

在你的第一个例子中:

(defn factorial [n] (if (< n 2) n (* n (factorial (dec n)))))

阶乘(factorial)符号被解析为变量#'user/factorial,随后由编译器评估该变量以获取其当前值——一个已编译的函数。这种评估仅在函数编译时发生一次。在第一个示例中,factorial是在定义函数时 #'user/factorial

在第二个示例中:

(defn factorial [n] (if (< n 2) n (* n (#'factorial (dec n)))))

您明确要求了Var #'user/factorial。调用一个Var的效果与解引用该Var并调用其(函数)值相同。这个示例可以更加明确地写成:

(defn factorial [n] (if (< n 2) n (* n ((deref (var factorial)) (dec n)))))

clojure.contrib.trace/dotrace 宏(我很久以前编写的)使用 binding 临时将 Var 重新绑定到不同的值。它不会更改任何函数的定义。相反,它创建一个调用原始函数并打印跟踪行的新函数,然后将该函数绑定到 Var。

在您的第一个示例中,由于原始函数是使用 valuefactorial 函数编译的,因此 dotrace 没有效果。在第二个示例中,每次调用 factorial 函数都会查找 #'user/factorial Var 的当前值,因此每次调用都会看到 dotrace 创建的替代绑定。


1
你又错了重点。真正的问题不在于变量与函数之间的区别。问题在于Clojure 1.2之前和1.2之间的变化,即将函数自身的名称在函数内部进行词法作用域限制,因此自调用不再通过变量进行。 - hiredman
谢谢!有没有办法让调用始终通过var进行,或者预测何时会以及何时不会?我发现当我想要改变事物时,它们不会改变,为了修复它,我必须不断地插入# '。这不仅仅是自我调用的问题。 - John Lawrence Aspden

9
为了避免人们对问题感到困惑,我来解释与Web开发相关的“问题”。这是Ring而不是Clojure的限制(实际上是Java Jetty库的限制)。您可以始终正常地重新定义函数。但是,提供给Jetty服务器进程的处理程序无法重新定义。您的函数正在更新,但Jetty服务器无法看到这些更新。在这种情况下,提供一个变量作为处理程序是解决方法。但请注意,该变量不是真正的处理程序。必须向Jetty服务器提供AbstractHandler,因此Ring使用代理创建一个闭合于处理程序的处理程序。这就是为什么要动态更新处理程序,它需要是var而不是fn的原因。

啊,这听起来可能是谜题的最后一块拼图。谢谢David。我现在会接受Joost的答案“它已经是了”。不过确实有一些复杂的东西在进行中... - John Lawrence Aspden
2
这个解决方法(可以这么说)有效,因为Var足够友好,会将任何.invoke调用转发给它所包含的东西。 - kotarak

8

我认为你错了。在Clojure 1.2中,你绝对可以重新定义函数,并且调用代码将调用新的定义。在1.3中,这似乎可能会有所改变,但1.3还没有完全固定。


Joost,我可能有点困惑。我编辑了这个问题以显示我认为是由于早期绑定导致的行为。你知道到底发生了什么吗?是dotrace里有什么好笑的东西吗? - John Lawrence Aspden
看起来函数体对它们自己的值有一个词法绑定(而不是它们的变量),你不能通过使用绑定来覆盖它,但我不确定这一点。如果您使用两个相互递归的函数,您的示例应该按照您的期望工作。顺便说一下,Clojure 不会自动进行尾调用优化,因此您的阶乘示例将很快耗尽堆栈。请参阅关于 recur 和 lazy 序列的文档以获取典型解决方案。 - Joost Diepenmaat
哦,这很有道理!虽然这似乎是一种奇怪的优化方式。也许这与匿名函数自调用能力有关。另一方面,我仍然不明白为什么我在博客文章中需要所有的#'(http://www.learningclojure.com/2011/03/hello-web-dynamic-compojure-web.html)。但是困惑比无知要好得多。非常感谢。问题再次编辑过。 - John Lawrence Aspden

8
在Clojure-1.3中,您还可以在运行时重新定义函数(从而更改根绑定),这仍然与1.2和1.1相同。但是,您需要将将动态重新绑定的变量标记为动��的binding。这种破坏性的变化提供了:
  • 显著的速度提升
  • 允许绑定通过pmap工作
  • 完全值得,因为99%的变量永远不会被重新绑定

听起来很不错,有点像Common Lisp的动态变量。但实际上我在问1.2的事情,我想我一定是对某些事情感到困惑了。有时候我似乎可以即时重新定义东西,有时候则不行。我稍微编辑了一下问题以展示我的意思。 - John Lawrence Aspden

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