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

欢迎!请查看关于页面以了解更多关于这个工作机制的信息。

+1投票
Clojure

bug CLJ-2094 展示了由像 partial 这样的函数引起的边缘案例,它们急切评估它们的参数并且因此保留过时的引用。我最近遇到了这个问题,发现因为“常规Clojure评估规则”而拒绝了这个工单。

我认为有一个简单的协议修复办法,它几乎不会增加开销,并允许协议相关函数在急切和懒加载评估环境中工作。团队愿意为此补丁感兴趣吗?

也许你可以提供更多关于“简单的修复方法”以及它对现有代码可能产生的影响的信息,这样人们就能更好地了解补丁实际上将涉及哪些内容?
同意Sean的说法,可以分享这个想法吗?

编辑了
lol 过去你们通常希望我在开始分发补丁前检查,所以我想在你们面前推代码之前先提出一个初步检测。

我已经签署了CLA

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

简而言之:在查看 `:imps` 的几个协议函数中取消 var 的引用,以便看到任何更新。
在我的criterium基准测试中,它大约每次调用增加+5ns。
如果我将调用移至最内部的调用点,性能成本也会降低。

1 个回答

0

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

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

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

所以您使用了一个无效函数,但在方法调用过程中查找以恢复活动函数的等效状态。在调用过程中添加var查找在我看来是一个失败的想法,因为整个协议设计都在试图避免这一点(这就是为什么我们有在修改时可以清空的缓存)。这里的替代解决方案是将函数的var查找推向调用者。这正是CLJ-2094的匿名函数实现规格(与partial相对)所提供的东西(vs部分)。

再次强调,我不确定我们真正试图解决的问题是什么。在CLJ-2094中,问题是函数被partial捕获。解决方案是——不要捕获值。我们也可以考虑一些替代的partial,它保留了var查找——您可以想象一个partial-var,它接受一个var而不是从符号中解析出的函数。解决方法是使用wrap在函数中延迟评估。这足够成为引入新技术的问题吗?不知道。

感谢你详细描述这种情况,Alex。我很欣赏你选择的词。

> 重复一遍,我不确定我们真正想要解决的问题是什么。在CLJ-2094中,问题是函数被partial捕获。解决方案是——不要捕获值。

我试图解决的问题是要使使用无效协议函数的行为与使用活动协议函数的行为相同。显然,由副作用函数(`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?`),并且与基本协议方法构建器不交互,因此现有的方法缓存将继续按目前的方式工作。

再次感谢您抽出时间撰写详细的评论。我真的很重视您在这些讨论中所付出的努力。
by
> 我试图解决的问题是要使死协议函数的使用与活协议函数的使用相同。

但是你为什么要这样做?我认为这不是问题。我不是说没有问题,但我不认为我们已经发现了真正的问题,继续试试吧。

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

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

> 这可能会导致在定义协议的命名空间和扩展协议的命名空间中定义协议时出现微妙错误,如果此时在引用较后命名空间之前对协议进行评估。

我认为这是真正开始接触问题的地方,这实际上是一个REPL过程问题。让我们试着说得明确点...有时候帮助分解目标和方法。

> 目标:在REPL中重新评估协议定义(为什么?命名空间重新加载?重新定义?),并继续工作。

> 障碍:我认为还需要继续挖掘,我们应该尽量具体(我认为当具体的时候,替代方案和解决方案将更加清晰)。发生了什么?为什么?我认为这是因为扩展协议的对象不再被视为扩展协议,并且不能再次调用协议方法。
by
> 目标:在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)。
没有方法::execute 在协议:#'user/CLJ-2094 中找到,对应的类:user.Foo
```

总结一下

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

反对意见
* 将存储在 `:var` 上的 var 解引用的速度将比当前代码路径慢。
* 这个问题实际上是关于引用与 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`解析到一个维持对其来源变量反向引用的实例——这样您就把简单值和状态纠缠在一起。

在开发过程中,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`,这使得它能够像其他函数那样工作。这是预期的行为。然而,向`extends?`(以及其他协议辅助函数)传递var会抛出NPE,因为它们不会解引用一个var。

也许可以将协议辅助函数修改为也能够处理vars,这将为这种情况提供一个渠道?
...