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查找—你可以想象一个接受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

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

很明显,这是当前的解决方案,也是一种在很多其他情况下都有效的方法(例如函数本身)。我认为这是不一致性造成的不必要的痛苦的地方,可以加以改进。

你说 recur 变量不可行是因为已经投入方法论缓存。你担心我的建议会被用在生成的方法中或类似的情况下吗?我的建议只会影响协议辅助函数(`extends?`、`extenders`、`find-protocol-method` 和 `satisfies?`)并且不会与基础协议方法构建器交互,因此现有的方法缓存会继续像现在那样工作。

再次感谢你抽出时间撰写详细评论。我真的很珍视你在这些讨论中付出的努力。
> 我试图解决的问题是在使用死协议函数和使用活协议函数工作方式一致。

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

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

> "不"吗?我认为是“是”,因为副作用。

> 如果在需要后续命名空间之间评估了协议,那么在定义协议的命名空间中定义并在其他命名空间中扩展协议可能会导致微妙的错误。

> 我认为这实际上正在接近问题所在,这实际上是一个 repl 进程问题。让我们尝试明确地说出来...有时候把目标和障碍分开是有帮助的。

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

> 障碍:我认为这需要更多的挖掘,我们应该尽量具体(我认为当它变得具体时,替代方案和解决方案将会更加清晰)。发生了什么?为什么?我认为问题在于扩展协议的对象不再看起来像扩展了协议,并且无法调用协议方法。
>  目标:在 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)。
没有在类`user.Foo`中找到对应协议`#'user/CLJ-2094`的方法::execute的实现。
```

总结一下

目标:扩展协议应该传播到协议的(非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)在用户/eval189 (REPL:1)。
```
这里我传递了一个协议函数的var给`partial`,这允许它像其他函数一样工作。这是预期的行为。然而,将var传递给`extends?`(以及其他协议辅助函数)会抛出NPE,因为它们不会解引用var。

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