为什么 Clojure 比其他 JVM 语言更支持热插拔?

Why is Clojure more hot swappable than other JVM languages?

我们几乎可以在运行时立即重新加载 Clojure 中的任何函数 and/or 变量。我们甚至可以更改方法签名。我们最多可以使用 Scala 或 Java 来使用 JRebel,它速度慢、商业化且受限。是什么让 Clojure 如此具有交互性? 在 Slack 中阅读此内容,我发现了以下评论,但我希望了解更多信息。指向 papers/articles 进一步澄清问题的链接也很受欢迎(尽管不是必需的)。

It’s mostly because the language is set up to be reloadable. Clojure has a var indirection for every function or top level variable definition which you can mutate, so you can redefine just one function while keeping the rest of your environment the same and carry on

.

following up on that - there's indirection when the function name is in the code, but for a long running function that took another function as an argument (eg. you passed a handler function to an http server process startup) you can get the benefits of var indirection by hand - by passing #'handler instead of handler but otherwise you don't get the reloading (without restarting the process that took that arg)

.

kind of

direct linking replaces var calls being compiled with direct calls (edited) the var path however still exists and NEW code can still invoke via the vars

您要问的关键在于 Clojure 如何识别函数并在运行时运行它们。首先,Clojure 函数被定义为 vars,这是它们的 JVM 根 class、Var.

的 Clojure 名称

Clojure 的运行时维护一个名为 NamespacesConcurrentHashMap。此映射具有 Symbol 个键(名称空间名称)和 Namespace 个值。每个 Namespace 依次有一个 AtomicReference 的 Clojure map(称为 "mappings"),它是动态类型的,但本质上有 Clojure Symbol 键(局部变量名称)和 Var 值。

当您调用 Clojure 函数时,它首先会查找您在 Namespaces 中引用的命名空间,然后在该命名空间的映射中查找特定变量。这使得热加载代码变得微不足道——您需要做的就是在给定命名空间的映射上设置一个新的 <Symbol, Var> 对。

为了更深入一点,Clojure 还保持对 "frames" 的感知(即线程或可能会在局部范围内临时重新定义变量的附加绑定)。它们有自己的 ThreadLocal 存储,并且将使用其中一个中的变量,而不是当前存储在命名空间映射中的变量。


Clojure 的方法在这里是可行的,因为它不会尝试将函数存储为 JVM 函数,而是将其存储为 Java 保存在可以快速访问的映射中的对象本身。

Clojure 通过检查它们是否满足函数接口 (IFn) 知道这些对象实际上是可调用的。对象通过具有 Invoke 方法来满足 IFn。这用于许多非常聪明的目的,并解释了为什么 Clojure 的许多核心数据结构(映射、向量、关键字等)也都可以作为函数调用。