请在Clojure 2024 调查问卷中分享您的想法!

欢迎!请查阅关于页面以了解该如何操作的相关信息。

+1
Clojure

错误CLJ-2094展示了由诸如partial之类的函数导致的协议边缘情况,这些函数急切求值其参数并且因此保留了过时的引用。我最近遇到了这个问题,发现由于“Clojure评估规则正常”而拒绝了这个工单。

我认为有一个简单的协议修复方案,只需增加很少的开销,并允许协议相关函数在急切和懒惰评估环境中正常工作。团队对此有兴趣吗?

也许您可以提供更多有关“简单修复”及其对现有代码可能造成的影响的信息,以便让大家对补丁可能涉及的内容有更深入的了解?
同意Sean的观点,您可以分享这个想法吗?

编辑了
lol 之前你们通常希望我在开始分发补丁之前先进行检查,所以我打算在把代码推到你们面前之前先做个检查。

我已经签署了CLA

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

简单来说:在查看`:imps`的几个协议函数中,重新引用协议上的var,以便可以看到任何更新。
在我的criterium基准测试中,每次调用大约会增加5纳秒。
如果我把调用移到最内层的使用点,性能开销也会减少。

1 条回答

0

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

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

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

因此,您正在使用一个已死去的函数,但在方法调用期间进行查找以恢复活动函数的等效状态。我认为在调用路径中添加 var 查找并不是一个好主意,因为整个协议设计试图避免这种情况(这也是为什么我们有可以在修改时清除的缓存)。这里的替代方案是将函数的 var 查找推到调用者那里。这正是您在 CLJ-2094 中通过匿名函数实现的规范(与部分相对)所得到的。

再次,我不确定我们实际上试图解决的问题是什么。在 CLJ-2094 中,问题是被部分捕获的函数。解决方案是——不要捕获值。我们还可以考虑一些可以保留 var 查找的部分替代方案——您可以想象一个用于接受 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?`),并且不会与基础协议方法构建器交互,因此现有的方法缓存将继续像目前一样工作。

再次感谢您花时间写下详细的评论。我非常珍视您在这些讨论中所付出的努力。
by
> 我试图解决的问题是要让已死协议函数的使用方式与活协议函数相同。

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

不明显的是,可以被副作用函数(`extend`)改变的协议实际上并不依赖于全局状态。

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

这可能会在在一个命名空间中定义协议并在其他命名空间中扩展它们时导致细微的错误,如果在引入后续命名空间之前协议被评估。

我认为这才是真正开始触及问题的所在,这实际上是一个REPL进程问题。让我们试图明确地说出来...有时候把目标和障碍分开是有帮助的。

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

障碍:我认为这需要更深入挖掘,我们应该尝试更具体(我认为一旦如此,可供选择和解决方案将更加清晰)。发生了什么?为什么?我认为扩展协议的对象不再看起来是扩展了协议,并且不能调用协议方法。
by
>  目标:在REPL中重新评估协议定义(为什么?命名空间重载?重定义?)并且扩展了这个协议的现有对象应该继续“工作”(我们可以更具体一些)

这不是当前问题的关键问题(尽管它也是一个具有类似来源的有效问题)。这不是关于重新评估协议定义,而是评估函数如`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)))
```
如果我调用`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)。
没有找到方法: :execute 的协议: #'user/CLJ-2094 的类: user.Foo 的实现
```

总结一下

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

反对意见
* 在`:var`上存储的var的解引用将比当前代码路径慢。
* 这个问题实际上更多是关于引用与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指向不依赖于全局状态的实例值具有很大价值(即,这是一个特性而不是缺陷)。根据您是使用`#foo`还是`foo`,您现在都有机会这样做。我不认为我们想让`foo`解析到一个维护着对其来源的var的回引用的实例 - 这样您就把一个简单的值和状态交织在了一起。

在开发过程中的REPL期间,存在相互竞争的利益,您希望现有实例“看到”新的定义。我想我们可能需要再次考虑一些使用这种情况的案例。也许我们在这个过程结束时又回到了这里,但这似乎与Clojure的一般风格相矛盾。
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,这将为此类情况提供一条途径?
...