请在2024年Clojure调查!中分享您的想法。

欢迎!请参阅关于页面以获取更多关于此工作方式的信息。

+1
Clojure

错误CLJ-2094演示了由如partial之类的函数导致的协议的边缘情况,因为这些函数会对它们的参数进行急切求值并因此保留过时的引用。我最近遇到了这个问题,并发现该票据已被拒绝,因为“正常Clojure评估规则”。

我认为协议的一个简单修复可以减少很少的开销,并允许与协议相关的函数在急切和懒惰评估上下文中工作。团队会对这个修补程序感兴趣吗?

也许您可以提供更多关于“简单修复”及其对现有代码可能产生的影响的信息,以便让大家对补丁可能涉及的实际情况有更多的了解?
同意Sean的观点,能否分享一下您的想法?
by
编辑 by
lol 在过去,你们通常喜欢我在开始分发补丁前检查,所以我打算在把代码推送给你们之前先做个基础检查。

我已签署CLA

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

简而言之:在查看 `:imps` 的几个协议函数中,重新 deref 协议上的 var,这样任何更新都可以被看到。
by
在我的criterium基准测试中,每次调用大约增加+5ns。
by
如果我把这些调用的位置移到最内部的调用点,性能开销也会减少。

1 条回答

0
by

这个问题更像是提出的解决方案而不是问题,这使得我很难将其当作工单提交。我不确定CLJ-2094是否是你们真正关心的问题,或者是否是其他问题?如果是其他问题,请分享。

尝试在这里对建议的解决方案进行文字描述...当找到一个协议方法匹配时,通过var查找当前的协议值(可能已经被新扩展更新过),而不是使用传递的协议值(因为它可能不再表示当前状态)。

但是协议已经具有在扩展添加到协议时重置其状态的机制,所以我认为这个问题已经被很大程度上解决了。唯一不会匹配当前状态的情况是,你拥有的协议状态不再连接。在这种情况下,我想这是因为协议函数var持有的是一个实例函数,该实例有一个缓存,其中包括协议,但函数实例本身就是旧的(var持有新的函数实例)。

因此,你正在使用一个无效的函数,但在方法调用期间进行查找以恢复活动函数的等效状态。在调用调用链中添加var查找似乎是不可行的,因为整个协议设计都试图避免这种情况(这就是为什么我们有可以在修改时清除的缓存)。这里的替代解决方案是将函数的var查找推送到调用者。这正是CLJ-2094(与部分实现相比)中的匿名函数实现规范的地方。

再次,我不确定我们实际上试图解决什么问题。在CLJ-2094中,问题是函数被部分捕获。解决方案是——不要捕获值。我们也可以考虑某种替代品,它可以保留var查找 —— 你可以想象一个partial-var,它接收一个var而不是从符号中评估的函数。解决方法是使用wrap在函数中延迟评估。这个问题是否足够重要,需要新的解决方法?不知道。

by
感谢您对情况的详细描述,Alex。我欣赏您的用词清晰。

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

我试图解决的问题是对无效协议功能的使用与对活动协议功能的使⽤保持一致。通过副作用函数(`extend`)可能会改变协议,这并不意味着它实际依赖于全局状态。当在同一个命名空间中定义协议并在其他命名空间中扩展它时,如果在扩展之间评估协议,可能会出现微妙的错误。(也就是说,在我的工作中遇到过这个问题。)

与多方法(multimethods)和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?`)并且不与基本协议方法构建器交互,所以现有的方法缓存将像现在一样继续工作。

再次感谢你花时间撰写详细的评论。我非常重视你为这些讨论付出的努力。
> 我试图解决的问题是要使死亡协议函数的使用与活协议函数的使用相同。

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

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

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

这可能导致在定义协议时一个命名空间和在其他命名空间扩展协议时(如果在要求的后续命名空间之间评估该协议)存在细微的错误。

我认为这才是真正开始接近问题所在,这实际上是一个图形处理器流程的问题。让我们尝试明确地说...有时有必要破除目标和障碍。

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

障碍:我认为这需要更深入的研究,我们应该尽量具体(我认为一旦具体,备选方案和解决方案就会变得很清晰)。会发生什么?为什么?我认为这可能是对象扩展的协议不再被视为扩展该协议,并且无法调用协议方法。
>  目标:在rep中重新评估协议定义(为什么?命名空间重新加载?重新定义?)并且扩展协议的现有对象应继续“工作”(我们能更具体一点吗?)

这并不是当前问题所在(尽管它也是一个具有类似来源的有效问题)。这不是关于重新评估协议定义,而是在`partial`或`let`绑定中评估协议var或协议函数var。因为当评估时将其解引用到底层对象,所以后续对`extend-protocol`的调用不会传播到早期的引用。

给定

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

(defrecord Foo [])

(def partial-extends (partial extends? CLJ-2094))
(def partial-execute (partial execute (->Foo)))
```
如果我在`Foo`上实现`CLJ-2094`时调用`extend-protocol`,`partial-extends`会返回false,并且`partial-execute`会抛出“未实现”异常

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

user=> (partial-extends Foo)
false
user=> (partial-execute :bar)
执行错误(IllegalArgumentException)在 user/eval145$fn$G (REPL:1).
没有实现方法::execute of协议:#'user/CLJ-2094 found for class: user.Foo
```

总结一下

目标:扩展协议应该传播到协议的非var引用(以及到所有协议辅助函数)。

反对意见
* Deref'ing存储在`:var`上的var会比当前代码路径慢。
* 这实际上是关于引用与vars的更大问题(cf. `(def fns {:key1 some-func :key2 other-func})` vs `(def fns {:key1 #'some-func :key2 #'other-func})`).
`extend-protocol`的全局状态实际上并不是全局的(与spec的全局注册表不同)。
`partial`实际上是一个很差的函数,应该尽量避免lol。

看起来怎么样?
by
我不认为这是一个令人向往的目标(但是与协议无关)。:)
by
如果你不介意,你认为为什么这不是一个令人向往的目标?
by
在Clojure中,全局状态机制由vars(用于多态方法、协议和规范注册表)实现。允许vars反引用到独立于全局状态的操作实例非常有价值(即,这是一个特性而不是错误)。您现在可以有机会根据是否使用`#foo`或`foo`来决定。我认为我们不想让`foo`解析为维护着来自其来源的变量回引的实例 - 这样您就把一个简单的值与状态交缠在一起了。

在REPL开发期间存在一些竞争利益,您希望现有实例“看到”新的定义,我认为我们将重新审视一些用于此目的的用例。也许在处理完这个过程后我们最终还会回到这里,但这似乎与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)在用户/eval189(REPL:1)。
```
在这里,我通过传递协议函数的var给`partial`,使其表现得与其他函数一样。这是预期行为。然而,传递一个var给`extends?`(及其他的协议辅助函数)会抛出NPE,因为它们不会反引用一个var。

也许协议辅助函数可以修改为也可以作用于vars,这将为此类情况提供一条途径?
...