2024 Clojure 状况调查!中分享你的想法。

欢迎!请查看关于页面以了解更多信息。

+1
Clojure

错误CLJ-2094演示了由如partial等函数引起的协议边缘情况,这些函数急切地评估其参数并因此保留过时引用。我最近遇到了这个问题,并发现由于“正常的Clojure评估规则”,该条目已被拒绝。

我认为有一种简单的协议修复方法,这种方法很少增加开销,并允许与协议相关的函数在急切和懒散评估环境中工作。团队对此补丁感兴趣吗?

也许您可以提供更多关于“简单修复”和它可能对现有代码造成的影响的信息,这样人们就可以更有洞察力地了解补丁可能涉及的内容?
我同意Sean的看法,你能分享这个想法吗?
by
编辑 by
lol 在过去,你们通常喜欢我在发补丁前先检查,所以我打算先在你们面前看看结果。

我已经签署了CLA

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

简而言之:在查看`:imps`的一些协议函数中对协议中的变量进行deref,以使任何更新都能被看到。
by
在我的criterium基准测试中,每次调用的大约额外5ns。
by
如果我将调用移至使用内部的最深处,性能成本也会降低。

1 答案

0
by

这个问题更像是一个解决方案的提议,而不是一个实际问题,这使得我很难将其作为工单提交。我不确定CLJ-2094是否是您真正关心的问题,还是其他问题?如果是其他问题,请分享。

在这里试着将提议的解决方案用文字表达出来……在找到协议方法匹配时,通过var检查当前协议值(这个值可能已被新扩展更新),而不是使用传入的协议值(因为这个值可能不再代表当前状态)。

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

因此,您正在使用一个废弃的功能,但在方法调用期间执行查找以恢复活动功能的等效状态。在调用路径中添加var查找我认为不是一个可行的方案,因为整个协议设计都试图避免这样做(这就是为什么我们有可以在修改时清空的缓存)。这里的替代方案是将函数的var查找推送到调用者。这正是CLJ-2094中规范的非匿名函数实现所获得的(与部分相反)。

同样,我不确定我们实际上试图解决什么问题。在CLJ-2094中,问题是函数被部分捕获。解决方案是不捕获值。我们也可以考虑一些替代方案,保留var查找——可以想象有一个部分-var,它接受一个变量而不是从符号计算出的函数。作为解决方案,可以使用wrap在函数中延迟评估。这足够成为一个新问题的理由吗?我不知道。

by
感谢你详细描述这个情况,亚历克斯。我欣赏你选择的用词之精确。

>> 同样,我不确定我们实际上试图解决什么问题。在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

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

显然,这是当前解决方案,也是许多其他实例(如函数本身)中的解决方案。我认为这是不一致性导致痛苦的地方,并且可以积极改进。

你说由于方法缓存中投入的努力,取消引用变量是不可行的。你担心我提出的更改会被用于生成的函数或类似的地方吗?我提出的更改仅影响协议辅助函数(`extends?`、`extenders`、`find-protocol-method` 和 `satisfies?`)并且不会与基本协议方法构建器交互,所以现有的方法缓存将继续按其目前的方式工作。

再次感谢你花费时间写出详细的评论。我真的很重视你为这些讨论所付出的努力。
> 我试图解决的问题是要使用死的协议函数和活的协议函数工作方式相同。

你为什么这样做?我认为这不是问题。(我不是说没有问题,但我不认为我们已经找到了真正的根源,继续努力。)

> 协议可以由副作用函数(`extend`)改变,这实际上并不依赖于全局状态,这一点并不明显。

"不?"我觉得是“是”,因为副作用

> 当在其中一个命名空间中定义协议并在另一个命名空间中扩展它时,如果协议在需要后续命名空间之间被评估,这可能导致微妙的错误。

我认为这才是真正开始触及问题的可能性,这实际上是一个 repl 流程问题。让我们尝试明确地说...有时分出目标和障碍是有帮助的。

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

障碍:我认为这需要更深入地挖掘,我们应该尽量具体(我认为当它变得具体时,备选方案和解决方案会变得更为清晰)。发生了什么?为什么?我认为这是对象不再扩展协议以及协议方法不能再被调用的原因。
>  目标:在 repl 中重新评估协议定义(为什么?命名空间重载?重新定义?)并且扩展该协议的现有对象应该继续“工作”(我们可以更具体一些吗?)

这个问题并非当下需解决的焦点(尽管它也是一个有类似起因的有效问题)。这不仅仅关于重新评估协议定义,而是评估在`partial`函数中或`let`绑定中的协议变量或协议函数变量。因为它是解除引用到基本对象的,所以任何后续对`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))

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

总结一下

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

反对意见
** 解除对存储在`:var`上的变量引用将比当前代码路径慢。
**这个问题实际上是关于引用和变量(参见`(def fns {:key1 some-func :key2 other-func})` vs `(def fns {:key1 #'some-func :key2 #'other-func})`)的更广泛的问题。
** `extend-protocol`的全局状态实际上不是全局的(与spec的全局注册不同)。
** `partial`实际上是一个糟糕的函数,应该避免使用lol。

看起来怎么样?
我不同意这是目标(但同意与协议无关)。 :)
如果你不介意,你为什么认为这不是一个理想的目标?
在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)))

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

也许可以将协议辅助函数修改为也可以封装vars,这将为这种 situation 提供一条路径?
...