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

欢迎!请参阅关于页面,了解更多该怎么做。

+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查找——你可以想象一个partial-var,它接收一个var而不是从符号中评估出的函数。一种解决方案是在函数中使用wrap来延迟评估。这是一个足够的问题需要新方案吗?我不知道。

by
感谢你详细描述情况,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 绑定中的作用域中评估协议变量或协议函数变量。因为它是references到在评估时的底层对象,所以后续对 `extend-protocol` 的调用不会传播到先前的引用。

给定

```
(defprotocol CLJ-2094
  (执行[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
  (执行[this bar] true))

用户=> (partial-extends Foo)
false
用户=> (partial-execute :bar)
执行错误(IllegalArgumentException)在用户/eval145$fn$G(REPL:1)。
找不到类:用户.Foo 的协议:# 'user/CLJ-2094 的方法::execute
```

总结一下

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

反对意见
* 变量存储在 `:var` 上的 deref' 将比当前的代码路径慢。
* 这实际上是一个关于 references 和 vars 的更普遍问题(cf. `(def fns {:key1 some-func :key2 other-func})` 与 `(def fns {:key1 #'some-func :key2 #'other-func})`)。
* `extend-protocol` 的全局状态实际上并非全局的(与 spec 的全局注册表不同)。
* `partial` 实际上只是一个糟糕的函数,应该避免 Lol。

看起来怎么样?
我不同意这是一个想要实现的目标(但与协议无关)。:)
如果你不介意,为什么你认为这并不是一个想要实现的目标?
在Clojure中,全局状态机制由vars(用于多态方法、协议和规范注册表)实现。允许vars解引用为独立于全局状态的实例值具有重要意义(即,这是一个功能,而不是错误)。根据是否使用`#foo`还是`foo`,你现在有机会同时做到这两点。我不认为我们希望`foo`解析为维护对其来源的vars的回引用的实例 - 你已经将你的简单值与状态交织在一起。

在开发过程中,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
  (执行[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)在用户/eval189(REPL:1)。
```
这里我传入了协议函数的var给`partial`,这允许它像其他函数一样工作。这是预期的行为。然而,将var传递给`extends?`(以及其他协议辅助函数)会抛出NPE,因为它们不会解引用var。

也许可以修改协议辅助函数以使它们 również能够处理vars,这将为此类情况提供一种途径?
...