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

欢迎!请参阅 关于 页面,获取更多关于这个工作原理的信息。

+1
Clojure

Bug CLJ-2094 展示了由如 partial 函数引起的协议的边缘情况,该函数会正向评估其参数,并因此保留了过时的引用。我最近遇到了这个问题,发现该问题已被拒绝,原因是“常规 Clojure 评估规则”。

我认为有一种简单的协议修复方法,这种方法的开销非常小,并允许与协议相关的函数在 eager 和 lazy 评估环境中工作。团队有兴趣修复这个补丁吗?

也许你可以提供更多关于“简单修复”及其对现有代码可能产生的影响的信息,这样人们就能更了解补丁可能涉及的内容?
与 Sean 意见一致,你能分享一下这个想法吗?
 avatar
编辑了
在以前,大家都喜欢我在打补丁之前进行检查,所以我会在把代码推送到大家面前之前做个 интерфейс检查。

我已经签署了 CLA。

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

简单来说:在查看 `:imps` 的几个协议函数中重新引用协议变量,以便看到任何更新。
 avatar
在我使用 criterium 的基准测试中,每次调用大约增加 5 纳秒。
 avatar
如果我把调用移到使用的最内层,性能成本也会降低。

1 个回答

0
 avatar

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

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

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

因此,您正在使用一个已死的函数,但在方法调用期间进行查找以恢复活动函数的等效状态。在调用路径中添加var查找我认为是不可行的,因为整个协议设计都在试图避免这种情况(这就是为什么我们有可以在修改时丢弃的缓存)。这里的替代方案是将函数的var查找推送到调用者。这正是CLJ-2094中规格说明的匿名函数实现(与分片相比)所得到的。

再次,我不确定我们实际上试图解决的问题是什么。在CLJ-2094中,问题是函数被分片捕获。解决方案是 - 不要捕获值。我们还可以考虑一些不保留var查找的分片替代方案 - 你可以想象一个partial-var,它接受一个var而不是从符号评估的函数。解决方法是使用wrap在函数中以延迟评估。这个问题是否足以引发新事物的出现?不知道。

感谢详细描述情况,Alex。我很欣赏你选择的词语清晰。

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

我试图解决的问题是使使用已死的协议函数与活协议函数的工作方式相同。当协议可以通过副作用函数(`extend`)更改时,可能并不明显,协议实际上并不依赖于全局状态。如果在其他命名空间中扩展协议时在定义协议之间评估协议,这会导致微妙的错误。(也就是说,在我工作的公司中我们已经遇到过这个问题。)

与多方法和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)))
```
如果我调用 `extend-protocol` 在 `Foo` 上实现 `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的方法::execute 的协议:#'user/CLJ-2094 实现。
```

总结如下

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

反对意见
* 引用存储在 `:var` 上的变数比当前的代码路径慢。
* 这个问题实际上是关于引用与变数的更普遍问题(参照 `(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(用于多方法、协议和规格注册表)实现的。允许vars解除引用到独立于全局状态的部分值的机制具有很高的价值(即,这是一个特性而不是bug)。您现在有两种选择:使用`#foo`或`foo`,是否立即执行这两种操作。我认为我们不希望`foo`解析为维持对它从该变量中来的反向引用的实例 - 您已经将简单值与状态复杂化。

在开发过程中,repl中存在竞争利益,您希望现有实例“看到”新的定义,我觉得我们将重新审视一些因为这些用例。也许我们将在该过程的最后回到这里,但这似乎与Clojure的一般特性相矛盾。
QUESTION? by
我同意您在这里说的话,但有几点不一致之处我认为需要引起注意。

1) 多方法和规格都拥有一个在解除引用后持续存在的“全局状态”版本。我不知道它们的实现方式,所以我不能说为什么,我只知道它与我预期的行为相符
```
(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)。
```
这里我传递了一个协议函数的var给`partial`,允许它与其他函数一样工作。这是预期行为。然而,将var传递给`extends?`(以及其他协议帮助函数)会抛出NPE,因为它们不会解除引用var。

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