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

欢迎!请查看关于页面获取更多关于如何使用本站的信息。

+1投票
Clojure

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

我认为有一种简单的协议修复方法,这种方法不会增加太多开销,并允许协议相关的函数在急切和延迟评估环境中一起工作。团队对修复补丁感兴趣吗?

或许您可以提供更多有关“简单修复”及其对现有代码可能产生的影响的信息,这样让人们可以更了解一下可能涉及的补丁。
同意Sean的意见,你可以分享这个想法吗?
by
编辑 by
lol 在之前,大家通常更喜欢我在发布补丁之前先进行检查,所以我觉得在把代码推送到你们面前之前,先发个内部检查贴会更好。

我已经签署了CLA

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

简而言之:在几个查看 `:imps` 的协议函数中,解除协议上的变量引用,以便看到任何更新。
by
在我的criterium基准测试中,每次调用大约增加+5ns。
by
如果我把调用移到内部使用点,性能成本也会降低。

1 个答案

0
by

这个问题更像是一个提议的解决方案,而不是一个问题,这让我不确定该把它作为工单提交到哪个地方。我不确定CLJ-2094是否是你真正关心的问题,还是其他什么问题?如果是其他问题,请分享。

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

但是,协议已经有了在添加扩展到协议时重置其状态的机制,所以我认为这个问题已经被基本解决。唯一不匹配当前状态的情况是,你有的协议状态不再连接。在这种情况下,我认为这是因为协议功能var持有的是一个函数实例,这个实例有一个缓存,里面有协议,但函数实例本身是旧的(var里持有的是新的函数实例)。

所以,你正在使用一个无效的函数,但在方法调用期间进行查找以恢复活动函数的等效状态。我认为将var查找添加到调用路径中并不是一个好主意,因为这 whole 协议设计都在试图避免这种情况(这就是为什么我们有缓存,可以被修改时清空)。这里的替代解决方案是将函数的var查找推送给调用者。这正是CLJ-2094中对规范匿名函数实现与局部函数(partial)实现相比所得到的结果。

再次,我不确定我们实际上试图解决什么问题。在CLJ-2094中,问题是函数被部分(partial)捕获了。解决方案是——不要捕获值。我们也可以考虑一些替代的局部函数(partial),它保留了var查找——你可以想象一个接受var而不是从符号中计算出的函数值的局部-var。解决方案是使用wrap在一个函数中延迟计算。这足够成为引入新功能的问题吗?我不知道。

by
感谢你对情况的详细描述,Alex。我欣赏你词汇选择的清晰性。

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

我试图解决的问题是在使用无效的协议函数时要使其与活动协议函数的工作方式相同。很明显,协议可以通过副作用函数(`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?`),并不会与基础协议方法构建器交互,所以现有的方法缓存会继续按当前方式工作。

再次感谢您花时间撰写详细的评论。我非常珍视您在这些讨论中付出的努力。
by
> 我试图解决的问题使无生命协议函数与有生命协议函数的使用方式一致。

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

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

“不”吗?我认为是“是”,因为副作用的原因

> 在一个命名空间中定义协议而在其它命名空间中扩展它,如果在要求后续命名空间之间评估协议的情况下,可能导致潜在的微妙错误。

我认为这实际上开始触及问题所在,这实际上是一个交互式解释器进程问题。让我们尝试明确地说……有时将目的和障碍分开是有帮助的。

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

障碍:我认为这需要更深入的挖掘,我们应该试图更加具体(我认为一旦做到了这一点,可供选择的方案和解决方案将会更加清晰)。会发生什么呢?为什么?我认为问题是由于扩展协议的对象不再看似扩展了协议,而且协议方法无法再被调用。
by
>  目标:在 repl 中重新评估协议定义(为什么?命名空间重新加载?重新定义?)以及扩展该协议的现有对象应继续“工作”(我们能更具体些吗?)

这不是我们真正的问题(尽管这也是一个类似来源的有效问题)。这不是关于重新评估协议定义,而是评估在 `partial` 或 let 绑定中类似 `protocol var` 或 `protocol function var` 的协议。因为在评估时会解引用到底层对象,所以接下来的对 `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)。
没有在类 `user.Foo` 中找到协议 `#user/CLJ-2094` 的方法 `:execute` 的实现。
```

总之

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

反对意见
* 使用 `def` 存储在 `:var` 上的变量比当前的代码路径要慢。
* 这个问题实际上更普遍地涉及引用与变量(与 `(def fns {:key1 some-func :key2 other-func})` 相比`(def fns {:key1 #'some-func :key2 #'other-func})`)。
* `extend-protocol` 的全局状态实际上并非全局(与 spec 的全局注册表不同)。
* `partial` 实际上是一个糟糕的函数,应该避免使用。

这看起来如何?
by
我不认为这是一个理想的目标(尽管与我同意协议无关)。
by
如果你不介意,为什么你不同意这是一个理想的目标?
"
Clojure中全局状态是由vars实现的机制(用于多态、协议和spec注册)。vars可解引用到独立于全局状态实例值的做法非常有价值(也就是说,这应该是一个特性而不是一个错误)。你现在可以根据是否使用`#foo`或`foo`来执行这两者之一。我认为不希望`foo`解析为维护对其来源vars的回指实例——你把一个简单的值与状态纠缠在一起。

在开发中的REPL阶段,存在着相互竞争的利益,你希望现有实例“看到”新的定义,我认为我们将重新审视为此目的的一些用例。也许我们最终会回到这里,但这似乎与Clojure的常规思路相悖。
by
我同意你的说法,但有几处不一致之处我认为值得注意。

1) Multimethods和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`,这使得它能够像其他函数一样工作,这是预期的行为。然而,向`extends?`(以及其他协议辅助函数)传递var会导致NPEs,因为它们不会解引用var。

也许协议辅助函数可以修改为也能在var上工作,这将为这种情况提供一个路径?
...