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

欢迎!请查看 关于 页面获取更多关于这个站点如何运作的信息。

+1 投票
》 在 Clojure

Bug CLJ-2094 展示了源自像 partial 这样的函数通过急切求值其参数并因此保留过时引用的边缘情况。我最近遇到这个问题,发现这个补丁由于“Clojure 评估规则正常”而被拒绝。

我认为有一个简单的协议修复方法,它增加了很少的开销,并允许协议相关函数在急切和延迟求值环境中都能正常工作。团队对此修复方案有兴趣吗?

》 在
也许您可以提供更多关于“简单修复”及其对现有代码可能产生的影响的信息,这样大家就可以稍微了解这个补丁的具体内容。
》 在
同意 Sean 的看法,您能否分享这个想法?

编辑了
历史上大家一般都喜欢我在发布补丁前进行检查,所以我想在把代码塞到你们面前之前先发一个感想检查。

我已经签署了CLA

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

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

1 答案

0 投票

这个问题更像是提出的解决方案而不是问题,这让我难以将其作为一个工单提交。我不确定CLJ-2094是不是你真正关心的问题,或者是其他某个问题?如果是其他问题,请分享一下。

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

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

所以你可能在使用一个已死亡的功能,但在方法调用期间进行查找以恢复活动等价状态。我认为在调用路径中添加var查找是不现实的,因为这与整个协议设计试图避免的是(这就是为什么我们有一个在修改时可以清空的缓存)。这里的替代方案是将函数的var查找推送到调用者。这正是CLJ-2094中对规范的非匿名函数实现所做的(与部分相对)。

同样,我不确定我们实际上试图解决的问题是什么。在CLJ-2094中,问题是函数被partial捕获。解决方案是不捕获值。我们也可以考虑一些替代partial的方法,保留var查找——你可以想象一个partial-var,它接受一个var,而不是从符号评估得到的函数。解决方法是使用wrap在一个函数中延迟评估。这个问题是否值得产生新的东西?我不知道。

by
感谢你详细描述了这种情况,Alex。我赞赏你选择的词语的清晰度。

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

我试图解决的问题是要确保使用已死亡的协议函数与使用活动协议函数相同。协议可以通过影响其函数(例如'extend')而改变,但实际上并不是依赖于全局状态。这可能导致在定义协议和一个命名空间中扩展它们,在另一个命名空间中评估协议之间的细微错误。 (也就是说,在我的工作中我们遇到了这个问题。)

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

    (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`)来更改,实际上并不依赖于全局状态。

"不?" 我认为是 "是",因为有了副作用

这可能导致在将协议定义在一个命名空间中并在其他命名空间中扩展它们时出现细微的错误,如果在重新启动命名空间之间评估协议。

我认为 actually really starting to get at the problem,这个问题实际上是一个 repl process thing. Let's try to say it explicitly... sometimes helpful to break into objective and obstacle.

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

障碍:我认为这需要更深入的挖掘,我们应该尝试更具体(而且我认为当它具体化时,替代方案和解决方案将更加清晰)。发生了什么?为什么?我认为这是因为扩展协议的对象看起来不再扩展该协议,并且无法再调用协议方法。
>  目标:在 repl 中重新评估协议定义(为什么?命名空间重新加载?重新定义?)并使扩展协议的现有对象继续“工作”(我们可以更具体吗)

这不是当前问题的核心(尽管它也是一个类似来源的有效问题)。这并不是要重新评估协议定义,而是评估类似`partial`函数或在`let`绑定中的协议变量或协议函数变量。因为它是引用底层对象,所以任何对`extend-protocol`的进一步调用都不会传播到那个早期的引用。

给定

```
(defprotocol CLJ-2094
  (execute [this bar]))

(defrecord Foo [])

(def partial-extends (partial extends? CLJ-2094))
(def partial-execute (partial execute (->Foo)))
```
如果我在`Foo`上调用`extend-protocol`以实现`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)在user/eval145$fn$G (REPL:1)。
未找到方法: :execute 的协议: #'user/CLJ-2094 在类: user.Foo 上的实现
```

总结一下

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

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

看起来如何?
by
我不同意这是理想的目标(但同意与协议无关)。 :)
by
如果你不介意,为什么你认为这并不是理想的目标?
by
在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
  (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,这将为这种情况下提供一条途径?
...