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

欢迎!请参阅关于页面以了解更多这个网站的工作方式。

+1
Clojure

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

我认为有一个简单的协议修复方法,它增加的负担很小,并允许协议相关函数在急切和懒惰评估环境中工作。团队是否对修复这个问题的补丁感兴趣?

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

编辑
lol 在之前,大家一般都喜欢我在弹补丁之前先检查一下,所以我觉得在把代码推给你们脸之前,先来个内心确认。

我已经签署了CLA

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

简而言之:在使用协议的几个函数中引用`:imps`的变量,以便看到任何更新。
在我的criterium基准测试中,每次调用大约增加+5ns。
如果我把调用移到使用点的最内层,性能损失也会减少。

1个回答

0

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

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

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

因此,您使用了一个已弃用的函数,但在方法调用期间进行查找以恢复实时函数的等效状态。在调用路径中添加var查找似乎是不可行的,因为整个协议设计试图避免这一点(这就是我们为什么有一个可以在修改时清除的缓存)。另一种解决方案是将函数的var查找推送给调用者。这正是CLJ-2094中匿名函数实现对规范的建议(相对于部分实现)所得到的。

再次,我不确定我们真正试图解决什么问题。在CLJ-2094中,问题是函数被部分捕获。解决方案是——不要捕获值。我们还可以考虑一些替代部分的方法,该方法保留了var查找——您可以考虑一个partial-var,它接受一个变量而不是从符号解析得到的功能。解决方案是在函数中使用wrap来延迟评估。这是否足够成为一个问题而需要新的解决方案?不知道。

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

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

我尝试解决的问题是要使使用已弃用的协议函数使用的效果与使用实时协议函数相同。协议可以通过具有副作用的函数(`extend`)改变,并不明显依赖于全局状态。这可能导致在定义一个命名空间中的协议并在其他命名空间中扩展它时出现微妙错误,如果在这种情况下协议被评估。 (换句话说,我的工作中遇到了这个问题。)

与多方法(multimethods)和spec相比,多方法和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`)进行更改的协议实际上并不是依赖于全局状态的。

“不?”我认为是“是”,因为副作用的缘故

这可能导致在定义协议的命名空间中以及在其他命名空间中扩展协议时出现微妙错误,如果协议是在 require 后的命名空间之间评估的话。

我认为这实际上开始触及问题所在,这实际上是一个 repl 进程的问题。让我们尝试明确地说…有时候把目标和障碍分开说很有帮助。

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

障碍:我认为这需要更多的挖掘,我们应该尽量具体(我认为当它具体时,选择方案和解决方案将会更加清晰)。会发生什么?为什么?我认为问题是扩展协议的对象不再显得扩展了协议,并且无法再调用协议方法。
>  目标:在 repl 中重新评估协议定义(为什么?命名空间重新加载?重新定义?)并且现有的扩展协议的对象应该继续“工作”(我们能更具体地说明吗?)

并非当前的问题(尽管也是一个具有相似来源的有效问题)。这并不是关于重新评估协议定义,而是评估函数中像`partial`或`let`绑定中的协议变量或协议函数变量。因为它在评估时会解引用到基础对象,所以任何进一步调用`extend-protocol`都不会传播到那个较早的引用。

给定

```
(定义协议 CLJ-2094
   (执行 [this bar]))

(定义记录 Foo [])

(定义部分扩展(部分扩展?CLJ-2094))
(定义部分执行(部分执行(->Foo)))
```
如果我调用`extend-protocol`来在`Foo`上实现`CLJ-2094`,`部分扩展`将返回false,`部分执行`将引发“未实现”异常

```
(扩展类型 Foo
   (CLJ-2094
    (执行 [this bar] true))

用户=> (部分扩展 Foo)
false
用户=> (部分执行 :bar)
执行错误(IllegalArgumentException)在用户/eval145$fn$G(REPL:1)。
未找到方法:协议:#'用户/CLJ-2094 的类:用户.Foo 的方法::执行
```

总结

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

异议
* 将存储在`:var`上的变量递归引用比当前代码路径要慢。
* 这个问题实际上更普遍地涉及到引用与变量(比较`(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` 解引用到一个维护对其来源 var 的反向引用的实例 - 您已经将简单的值与状态纠缠在一起。

在开发期间的 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` 伎俩
```
(定义协议 CLJ-2094
   (执行 [this bar]))

(定义记录 Foo [])

(def partial-extends (partial extends? #'CLJ-2094))
(def partial-execute (partial #'execute (->Foo)))

用户=> (部分执行 :bar)
true
用户=> (部分扩展 Foo)
执行错误 (NullPointerException) at user/eval189 (REPL:1)。
```
在这里,我将协议函数的 var 传递给 `partial`,这使得它能够像其他函数一样工作。这是预期行为。然而,将 var 传递给 `extends?`(以及其他协议辅助函数)会引发 NPEs,因为它们不会解引用 var。

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