如何在REPL中重新加载Clojure文件

191
在Clojure中重新加载已定义的函数的首选方法是什么,而不必重新启动REPL。目前,为了使用更新后的文件,我必须执行以下操作:
  • 编辑src/foo/bar.clj
  • 关闭REPL
  • 打开REPL
  • (load-file "src/foo/bar.clj")
  • (use 'foo.bar)
此外,(use 'foo.bar :reload-all)没有产生所需的效果,即计算修改后的函数体并返回新值,而不是像源代码根本没有更改一样行事。 文档:

21
对我来说,(use 'foo.bar :reload-all) 始终有效。如果您的类路径设置正确,则 (load-file) 永远不应该是必需的。你所说的“必需效果”是什么? - Dave Ray
是的,“必要效果”是什么?请在示例bar.clj中详细说明“必要效果”。 - Sridhar Ratnakumar
1
所谓的“必要效果”是指,如果我有一个函数 (defn f [] 1),并将其定义更改为 (defn f [] 2),那么在我发出 (use 'foo.bar :reload-all) 并调用 f 函数后,它应该返回 2 而不是 1。不幸的是,对我来说它并不是这样工作的,每次我更改函数体时都必须重新启动 REPL。 - pkaleta
你的设置中可能还有其他问题... :reload:reload-all 都应该可以工作。 - Jason
你能否发布一份REPL的记录? - Jason
8个回答

226

或者(use '你的命名空间 :reload)


4
":reload-all" 也应该有效。虽然OP明确表示无效,但我认为OP的开发环境可能还有其他问题,因为对于单个文件,两者(:reload:reload-all)应具有相同的效果。下面是 :reload-all 的完整命令: (use 'your.namespace :reload-all)这也会重新加载所有依赖项。 - Jason

82

还有一种替代方案,可以使用tools.namespace,它非常高效:

user=> (use '[clojure.tools.namespace.repl :only (refresh)])

user=> (refresh)

:reloading (namespace.app)

:ok

3
这个答案更合适。 - Bahadir Cambel
12
注意:运行 (refresh) 命令也会导致 REPL 忘记你已经要求了 clojure.tools.namespace.repl。接下来的 (refresh) 调用将抛出一个 RuntimeException,"Unable to resolve symbol: refresh in this context."。最好的做法可能是要么使用 (require 'your.namespace :reload-all) 命令重新加载命名空间,要么,如果你知道你将要为给定项目频繁地刷新 REPL,就可以创建一个 :dev 配置文件并将 [clojure.tools.namespace.repl :refer (refresh refresh-all)] 添加到 dev/user.clj 中。 - Dave Yarwood
1
Clojure工作流程重新加载的作者发布的博客文章:http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded - David Tonhofer

72
使用(require … :reload):reload-all重新加载Clojure代码是非常有问题的
如果您修改了互相依赖的两个命名空间,您必须记得以正确的顺序重新加载它们,以避免编译错误。 如果您从源文件中删除定义,然后重新加载它,这些定义仍然在内存中。如果其他代码依赖于这些定义,它将继续工作,但下次重新启动JVM时会出现问题。 如果重新加载的命名空间包含defmulti,则还必须重新加载所有相关的defmethod表达式。 如果重新加载的命名空间包含defprotocol,则还必须重新加载实现该协议的任何记录或类型,并用新实例替换任何现有实例。 如果重新加载的命名空间包含宏,则还必须重新加载使用这些宏的任何命名空间。 如果运行程序包含闭合在重新加载的命名空间中的值的函数,则这些闭合的值不会更新。(这在构造“处理程序堆栈”的Web应用程序中很常见,其中将函数组合起来。)

clojure.tools.namespace库显著改善了情况。它提供了一个易于使用的刷新功能,可以基于名称空间的依赖图进行智能重新加载。

myapp.web=> (require '[clojure.tools.namespace.repl :refer [refresh]])
nil
myapp.web=> (refresh)
:reloading (myapp.web)
:ok

很遗憾,如果你引用的命名空间发生了改变,第二次重新加载就会失败。这是因为tools.namespace在加载新代码之前会销毁当前版本的命名空间。
myapp.web=> (refresh)

CompilerException java.lang.RuntimeException: Unable to resolve symbol: refresh in this context, compiling:(/private/var/folders/ks/d6qbfg2s6l1bcg6ws_6bq4600000gn/T/form-init819543191440017519.clj:1:1)

您可以使用完全限定的变量名作为解决此问题的方法,但我个人更喜欢不必在每次刷新时键入它。上述方法的另一个问题是,在重新加载主命名空间后,标准REPL帮助函数(如docsource)不再在其中引用。
为了解决这些问题,我更喜欢为用户命名空间创建一个实际的源文件,以便可以可靠地重新加载。我将源文件放在~/.lein/src/user.clj中,但您可以将其放在任何地方。该文件应在顶部ns声明中要求刷新函数,如下所示:
(ns user
  (:require [clojure.tools.namespace.repl :refer [refresh]]))

你可以在~/.lein/profiles.clj中设置一个Leiningen用户配置文件,这样你放置文件的位置就会被添加到类路径中。配置文件应该看起来像这样:
{:user {:dependencies [[org.clojure/tools.namespace "0.2.7"]]
        :repl-options { :init-ns user }
        :source-paths ["/Users/me/.lein/src"]}}

请注意,我在启动REPL时将用户命名空间设置为入口点。这可以确保REPL辅助函数在用户命名空间中被引用,而不是您的应用程序的主命名空间中。这样,除非您更改我们刚刚创建的源文件,否则它们不会丢失。
希望这能帮到您!

@fl00r 抱歉,我不是很明白你的意思。在这种情况下应该是“source-paths”,因为我们正在加载源文件而不是其他资源。 - Dirk Geurs
2
@DirkGeurs,使用 :source-paths 我得到了 #<FileNotFoundException java.io.FileNotFoundException: Could not locate user__init.class or user.clj on classpath: > 的错误,但是使用 :resource-paths 一切正常。 - fl00r
1
@fl00r,你还是遇到了那个错误吗?你在启动REPL的文件夹中有一个有效的project.clj文件吗?这可能会解决你的问题。 - Dirk Geurs
1
是的,这很标准,在“:resource-paths”中一切正常,我在repl中的用户命名空间中。 - fl00r
1
我刚刚使用一个REPL工作了一段时间,但由于“reload”问题,它一直在欺骗我。后来发现,我认为一切正常的东西都不再起作用了。也许有人应该解决这种情况? - Alper
显示剩余12条评论

53

最佳答案是:

(require 'my.namespace :reload-all)

这不仅会重新加载您指定的命名空间,还会重新加载所有依赖的命名空间。

文档:

require


3
这是唯一适用于 lein repl、Clojure 1.7.0 和 nREPL 0.3.5 的答案。如果您是初学者:命名空间('my.namespace)在 src/.../core.clj 中通过 (ns ...) 定义。 - Aaron Digulla
1
这个答案的问题在于原始问题使用的是(load-file ...),而不是require。她如何在load-file之后向命名空间添加:reload-all? - jgomo3
由于命名空间结构(如proj.stuff.core)反映了磁盘上的文件结构(如src/proj/stuff/core.clj),因此REPL可以定位到正确的文件,您不需要使用load-file - Alan Thompson

6

基于papachan的回答,一句话简述如下:

(clojure.tools.namespace.repl/refresh)

5

我在Lighttable(和令人惊叹的instarepl)中使用这个功能,但它也应该适用于其他开发工具。在重新加载后,我遇到了旧函数和多方法定义仍然存在的同样问题,因此现在在开发过程中,不是通过以下方式声明命名空间:

(ns my.namespace)

我像这样声明我的命名空间:

(clojure.core/let [s 'my.namespace]
                  (clojure.core/remove-ns s)
                  (clojure.core/in-ns s)
                  (clojure.core/require '[clojure.core])
                  (clojure.core/refer 'clojure.core))

看起来有点丑陋,但每当我重新评估整个命名空间(在Lighttable中按Cmd-Shift-Enter以获取每个表达式的新instarepl结果),它会清除所有旧定义并给我一个干净的环境。 在我开始这样做之前,我每隔几天就被旧定义绊倒了,这挽救了我的理智。 :)


3

再试一次load-file?

如果您正在使用集成开发环境(IDE),通常有一个键盘快捷键可以将代码块发送到REPL,从而有效地重新定义相关函数。


1

一旦(use 'foo.bar)对你起作用,这意味着你的CLASSPATH上有foo/bar.clj或foo/bar_init.class。bar_init.class将是bar.clj的AOT编译版本。如果你执行(use 'foo.bar),我不确定Clojure是更喜欢class还是clj文件。如果它更喜欢class文件并且你有两个文件,那么很明显编辑clj文件然后重新加载命名空间没有任何效果。

顺便说一下:如果你的CLASSPATH设置正确,你不需要在use之前load-file

顺便说一下2:如果你因为某种原因需要使用load-file,那么如果你编辑了文件,可以简单地再次执行它。


14
不确定为什么这个被标记为正确答案,它没有清晰地回答问题。 - AnnanFay
5
作为回答此问题的人,我认为这个答案不是很清晰易懂。 - ctford

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