Clojure 2024 年状态调查 中分享您的想法!

欢迎!请查阅 关于 页面以获取更多关于如何使用本站的信息。

+1 支持
Clojure

错误 CLJ-2094 展示了由于像 partial 这样的函数急切评估其参数并因此保留过时引用的边缘情况。我最近遇到了这个问题,发现该工单被拒绝,原因是“Clojure 正常评估规则”。

我认为协议的一个简单的修复方法可以带来非常小的开销,并允许协议相关的函数在急切和懒加载评估环境中都工作。团队是否会对此修复感兴趣?

也许您可以提供更多关于“简单的修复方法”及其对现有代码的影响的信息,这样人们就可以对此次补丁可能涉及的实际情况有更多了解。
同意 Sean 的观点,能否分享这个想法?
by
编辑了 by
lol 在过去,你们通常希望我在开始推送补丁之前进行检查,所以我决定在把代码推给你们脸上之前先打个草稿。

我已签署了 CLA

https://github.com/NoahTheDuke/clojure/pull/4

简而言之:在查看 `:imps` 的几个协议函数中对协议中的变量进行解除引用,以便看到任何更新。
by
在我使用 criterium 进行基准测试时,每次调用大约增加 5ns。
by
如果我将调用移动到使用路径的内部点,性能成本也会降低。

1 个回答

0
by

这个问题更多的是一个建议的解决方案,而不是一个问题,这使得我很难把它作为票据文件。我不确定 CLJ-2094 是否是您关心的真正问题,还是其他问题?如果是其他问题,请分享。

在此尝试用文字描述建议的解决方案...在找到协议方法匹配时,通过变量查找当前的协议值(该变量可能已被新扩展更新过),而不是使用传递的协议值(因为这可能不再代表当前状态)。

但协议已经有机制在添加扩展到协议时重置其状态,所以我认为这个问题已经基本解决。唯一协议状态不匹配当前状态的情况是,您拥有的协议状态不再连接。在这种情况下,我认为这是因为协议功能变量持有的是一个函数实例,该实例有一个缓存,其中包含协议,但该函数实例本身是旧的(变量持有新的函数实例)。

因此,您正在使用一个已死的函数,但在方法调用期间执行查找以恢复活动函数的等效状态。我认为在调用调用路径中添加变量查找是不切实际的,因为整个协议设计都在试图避免这一点(这就是为什么我们有可以在修改时排空的缓存)。这里的另一种解决方案是将函数的变量查找推向调用者。这正是CLJ-2094中规范匿名函数实现(与部分函数相对)所获得的内容。

再次强调,我并不清楚我们真正试图解决的问题是什么。在CLJ-2094中,问题是函数被部分函数捕获。解决方案是不要捕获值。我们还可以考虑一些替代方案,比如部分函数保留了变量查找 - 你可以想象一个接部分变量的函数,它接受一个变量而不是从符号中评估出的函数。一种权宜之计是使用包装在函数中延迟评估。这足够成为新功能的问题吗?不知道。

by
感谢您详细描述这种情况,Alex。我非常欣赏您选择的言辞的清晰度。

> 再次强调,我不确定我们真正试图解决的问题是什么。在CLJ-2094中,问题是函数被部分函数捕获。解决方案是——不要捕获值。

我试图解决的问题是要让使用已死的协议函数与使用活动协议函数的行为一致。由副作用函数(“extend”)修改的协议不被视为依赖于全局状态这一点并不明显。这在将协议在一个命名空间中定义并在其他命名空间中扩展时可能会导致微妙的错误,因为在需要后续命名空间之间的协议评估。(也就是说,在我们的工作中我们遇到了这个问题。)

与多方法 spec 相比,多方法和 spec 具有全局状态,并且不依赖于它们的评估顺序。

    (defmulti example (fn [a b] [(:type a) (:type b)]))

    (def partial-multi-2094 (partial example {:type :foo}))

    (defmethod example [:foo :bar] [a b] {:a a :b b})

    (partial-multi-2094 {:type :bar :extra :keys})
    ;=> {:a {:type :foo} :b {:type :bar :extra :keys}}

    (require '[clojure.spec.alpha :as s])

    (def partial-valid-2094 (partial s/valid? ::clj-2094))

    (s/def ::clj-2094 (fn [x] true))

    (partial-valid-2094 :a)
    ;=> true

> 解决方案是——不要捕获值。

显而易见,这是当前解决方案,并且在许多其他情况下(例如函数本身)也是有效的工作。我认为这是一个不一致性导致过多痛苦的地方,可以做出积极的改进。

您提到取消引用 var 不可行,因为已经投入了大量精力到方法缓存中。您担心的是否我的提议的更改会应用于生成的函数或类似情况?我提议的更改仅影响协议辅助函数(`extends?`、`extenders`、`find-protocol-method` 和 `satisfies?`),并且不与基本协议方法构建者交互,因此现有的方法缓存将继续像目前一样工作。

再次感谢您抽出时间提供详细的评论。我真的很重视您在这方面投入的努力。
by
> 我试图解决的问题是让使用已死亡的协议函数与活协议函数的行为一样。

但是为什么你要这么做呢?我觉得这不是问题。 (我不是说没有问题,但我认为我们还没有找到真正的根源,继续探索吧。)

> 显然,可以通过副作用函数(`extend`)更改的协议并不依赖于全局状态。

“不是吗”?我认为“是”,因为副作用

> 如果在单个命名空间中定义协议并在其他命名空间中扩展它们,且协议在需要后续命名空间之间评估,这可能会引起微妙错误。

我认为这正是正在触及问题的根源,这实际上是一个 repl 过程问题。让我们尝试明确地说…有时候把目标和障碍分开是有帮助的。

目标:在 repl 中重新评估协议定义(为什么?命名空间重新加载?重新定义?)以及扩展协议的现有对象应继续“工作”(我们可以更具体一点吗?)

障碍:我认为这需要进一步的挖掘,我们应该尽量具体(我认为当它具体化时,备选方案和解决方案将变得更加清晰)。发生了什么?为什么?我认为是由于扩展协议的对象不再显示为扩展协议,而且无法再调用协议方法。
by
>  目标:在 repl 中重新评估协议定义(为什么?命名空间重新加载?重新定义?)以及扩展协议的现有对象应继续“工作”(我们可以更具体一点吗?)

这不是我们面临的问题(尽管这也是一个类似来源的有效问题)。这个问题不是关于重新评估协议定义,而是在 `partial` 或 let 绑定等函数中评估协议 var 或协议函数 var。因为评估时分解析到基础的 object,任何进一步的 `extend-protocol` 调用都不会传播到那个早期的引用。

给定

```
(defprotocol CLJ-2094
  (execute [this bar]))

(defrecord Foo [])

(定义部分扩展函数 (partial-extends extends? CLJ-2094))
(定义部分执行函数 (partial-execute (->Foo)))
```
如果我在Foo上调用`extend-protocol`来实现`CLJ-2094`,`partial-extends`将返回false,而`partial-execute`将抛出"未实现"异常

```
(extend-type Foo
  CLJ-2094
  (execute [this bar] true))

user=> (partial-extends Foo)
false
user=> (partial-execute :bar)
执行错误(IllegalArgumentException)在用户/eval145$fn$G (REPL:1)。
没有在类:user.Foo中找到协议:#'user/CLJ-2094的方法::execute的实现
```

总之

目标:扩展协议应传播到协议的非变引用地,以及到所有协议辅助函数。

反对意见
* 在`:var`上反引用的var比当前代码路径慢。
* 这个问题实际上是关于引用与vars的更一般性问题(参照`(def fns {:key1 some-func :key2 other-func})`与`(def fns {:key1 #'some-func :key2 #'other-func})`)。
* `extend-protocol`的全局状态实际上并非全局(与spec的全局注册表不同)。
* `partial`实际上是一个糟糕的函数,应该避免使用。

看起来如何?
by
我不同意这是一个理想的目标(但这与协议无关)。 :)
by
如果你不介意,你为什么认为这不是一个理想的目标?
by
Clojure中全局状态的机制是vars(它们用于多方法、协议和spec注册表)。允许vars解引用到独立的实例值,而不是全局状态,这是一种很有价值的特性。这本身就是一种特性而不是错误。你现在就有机会正确使用`#foo`或`foo`。我认为我们不想让`foo`解析为维护对它所来自的var的回引用的实例 - 因为你已经把一个简单的值与状态复杂化了。

在开发期间,复制中存在相互冲突的利益,你希望现有实例“看到”新定义,我认为我们将重新审视一些用例以实现这个目的。也许在那个过程的最后我们会回到这里,这似乎与Clojure的一般原则相悖。
by
我同意你在这里所说的,但是我认为有两点不一致值得注意。

1) 多态方法和Spec都有一版“全局状态”,它在每次解引用后持续存在。我不知道它们是如何实现的,所以我无法解释原因,我只知道它们按我预期的那样工作
```
(defmulti mm :type)
(def partial-mm (partial mm))
(defmethod mm :foo [_] ::bar)

user=> (partial-mm {:type :foo})
:user/bar

(require '[clojure.spec.alpha :as s])
(def partial-valid? (partial s/valid? ::foo))
(s/def ::foo map?)

user=> (partial-valid? {})
true
```
考虑到`extend`(以及其他宏)不需要你存储修改后的协议,只有一个“全局状态”不通过解引用工作,这让人感到意外。

2) 假设协议的全局状态行为(通过`extend`等)是有意的,协议辅助函数不作用于vars,这意味着不可能依靠老旧的`#'foo`技巧
```
(defprotocol CLJ-2094
  (execute [this bar]))

(defrecord Foo [])

(def partial-extends (partial extends? #'CLJ-2094))
(def partial-execute (partial #'execute (->Foo)))

user=> (partial-execute :bar)
true
user=> (partial-extends Foo)
执行错误(NullPointerException)在用户/评估189(REPL:1)。
```
这里我向`partial`传递了一个协议函数的var,这允许它像其他函数一样工作。这是预期的行为。然而,向`extends?`(以及其他协议辅助函数)传递var会引发NPE,因为它们不会解引用var。

也许可以将协议辅助函数修改为也可以在vars上工作,这将为这种情况提供一个途径?
...