请分享您的看法,参与2024年Clojure调查!

欢迎!请参阅关于页面,了解有关此工作的更多信息。

+1
Clojure

错误CLJ-2094演示了由诸如partial之类的函数引起的边缘情况,这些函数会急切地评估它们的参数并因此保留过时的引用。我最近遇到了这个问题,并发现该工单被拒绝,因为“正常的Clojure评估规则”。

我认为有一个简单的协议修复方案,这几乎不会增加开销,并且允许与协议相关的函数在急切和懒惰评估上下文中工作。团队对这个补丁感兴趣吗?

也许您可以提供更多有关“简易修复”及其对现有代码可能产生的影响的信息,这样人们就可以更加了解该补丁的实际内容?
我同意Sean的看法,你能分享这个想法吗?
lol,过去大家一般偏好我在分发补丁之前进行检测,所以我觉得在把代码塞给你们之前先来做个理性评估。

我已经签署了CLA

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

简单来说:在查看`:imps`的两个协议函数中,重新引用协议上的var,以看到任何更新。
在我的criterium基准测试中,大体上每次调用增加约5ns。
如果我将调用移动到使用点最内部,性能成本也会降低。

1 答案

0投票

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

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

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

因此,您正在使用一个死去的函数,但在方法调用期间查找以恢复活动功能的等效状态。在调用路径中添加var查找在我看来是一个不可行的方案,因为整个协议设计都在尝试避免这一点(这就是为什么会有一个可以在修改时抛出的缓存)。此处的替代解决方案是将函数的var查找推进给调用者。这正是CLJ-2094中对匿名函数规范实现(与部分相对)所得到的结果。

再次,我不确定我们实际上要解决的问题是什么。在CLJ-2094中,问题是将函数部分捕获。解决方案是——不要捕获值。我们还可以考虑一些替代方案,例如部分保留了var查找——你可以想象一个接受var而不是从符号中计算的函数的partial-var。作为 workaround,可以在函数中使用 wrap 来延迟计算。这个问题是否足够严重,需要一些新的内容?不知道。

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

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

我要解决的问题是要使用死的协议函数与活协议函数使用效果相同。协议可以由副作用函数(`extend`)更改,实际上并不依赖于全局状态,这一点并不明显。如果定义协议在一个命名空间中并在其他命名空间中扩展它,如果在中间进行协议评估,这可能导致微妙错误。(也就是说,在我们的工作中遇到过这个问题。)

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

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

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

空格(定义方法example [:foo :bar] [a b] {:a a :b b})

空格(部分多2094 {:type :bar :extra :keys})
;;=> {:a {:type :foo} :b {:type :bar :extra :keys}}

空格(引入 '[clojure.spec.alpha :as s])

空格(定义partial-valid-2094 (部分 s/valid? ::clj-2094))

空格(定义 ::clj-2094 (函数 [x] true))

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

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

显然,这是当前的解决方案,并且在许多其他情况(例如函数本身)中都适用。我认为这通常是导致造成过多痛苦的不一致性之处,并且可以进行积极的改变。

您说取消引用var不可取,因为已经投入了方法缓存的工作。您担心我的建议会被用于生成方法或类似方法中吗?我的建议仅影响协议辅助函数(`extends?`、`extenders`、`find-protocol-method` 和 `satisfies?`)且不与基协议方法构建器交互,因此现有方法缓存将继续像现在一样工作。

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

>>但是你为什么要这样做?我认为这不是问题所在。(我不是说我没有发现问题,但我觉得我们还没有找到真正的问题,继续努力。)

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

>>"aren't"?(不是意味着“are”,因为存在副作用)

>>如果一个协议在一个命名空间中定义,而在另一个命名空间中扩展它,如果在这个过程中协议被评估,则这可能会导致微妙的错误。

我认为这实际上开始接触到问题所在,而这实际上是一个解释器过程问题。让我们尽力明确地说出...有时有助于将目标和障碍分开。

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

障碍:我认为需要更深入挖掘,我们应该尽可能具体(我认为如此,替代方案和解决方案将更加清晰)。发生了什么?为什么?我认为这是因为扩展协议的对象不再被视为扩展协议,而且无法再调用协议方法。
>  目标:在 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 deref 的速度将比当前的代码路径慢。
* 这实际上是一个关于引用与 vars 的问题(例如,(def fns {:key1 some-func :key2 other-func}) 与 (def fns {:key1 #'some-func :key2 #'other-func}) 的比较)。
* `extend-protocol` 的全局状态并非全局(与 spec 的全局注册表不同)。
* `partial` 实际上是一个糟糕的函数,应该避免。

看起来怎么样?
by
我不同意这是理想的目标(但我同意与协议无关)。 :)
by
如果你不介意,为什么你觉得这不是一个理想的目标?
Clojure 中全局状态机制是 vars(用于多态方法、协议和规范注册表)。允许 vars 解引用到与全局状态无关的实例值非常有价值(即,这是一个特性而不是错误)。您现在可以根据是否使用 `#foo` 或 `foo` 来做这两件事。我认为我们不希望 `foo` 解引用到一个维护对其来源 var 的反向引用的实例 - 您已经将简单的值与其状态纠缠在一起。

在开发过程中,REPL 有相互竞争的利益,您希望现有的实例“看到”新的定义,我认为我们将重新审视一些为这种目的而使用的用例。也许在这个过程中结束时我们会回到这里,但这似乎与 Clojure 的一般宗旨相违背。
我同意你的看法,但有几点不一致之处我认为值得注意。

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)在 user/eval189 (REPL:1).
```
在这里,我正在将协议函数的 var 传递给 `partial`,这使得它像一个函数一样工作。这是预期行为。然而,将 var 传递给 `extends?`(和其他协议辅助函数)会抛出 NPEs,因为它们不会解引用 var。

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