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

欢迎!请参见关于 页面以获取更多关于如何使用本网站的信息。

+1
Clojure

bug 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查找推向调用者。这基本上就是CLJ-2094中对规范进行匿名函数实现时得到的内容(与部分不同)。

再次强调,我不确定我们实际上在试图解决什么问题。在CLJ-2094中,问题是函数被部分捕获。解决方案是——不要捕获值。我们还可以考虑一些替代方案,比如部分保留了var查找——你可以想象一个部分-var,它接受一个变量而不是从符号评估得到的函数。绕过方法是使用函数中的wrap来延迟评估。这足以证明需要一些新内容吗?我不知道。

感谢你对这种情况的详细描述,Alex。我感谢你的用词清晰。

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

我要解决的问题是要让使用无效的协议函数的效果与使用活协议函数相同。当协议可以通过副作用函数(例如,`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

> 解决方案是 - 不捕捉值。

显然,这是当前的解决方案,并且在其他许多情况下都适用(例如函数本身)。我认为这是不一致性导致不必要的痛苦的地方,可以被积极改进。

您说撤销var是不行的,因为已经投入了方法缓存的工作。您担心我提出的更改会用于生成的函数或类似的情况吗?我提出的更改仅影响协议辅助函数(`extends?`、`extenders`、`find-protocol-method`和`satisfies?`),并且不与基协议方法构建器交互,因此现有的方法缓存会继续像目前一样工作。

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

但您为什么这么做?我认为这不是问题。 (我并不是说没有问题,但我认为我们还没有找到真正的症结,继续努力。)

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

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

> 这可能导致在定义命名空间中的协议并在其他命名空间中扩展它们时出现微妙的错误,如果在这种情况下协议在要求后的命名空间之间被评估。

我认为这实际上是开始触及问题的真正所在,这实际上是一个重放过程的事情。让我们尝试明确地说出来...有时候把目标和障碍分离开来是有帮助的。

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

总结一下

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

反对意见
* 在 `: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` 解引用到一个保持对来源 vars 的回引用的实例——这样一来,您就把简单值和状态纠缠在一起了。

在开发期间的 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) 在用户/评估189 (REPL:1)。
```
在这里,我正在将协议函数的 var 传递给 `partial`,这使得它可以像其他函数一样工作。这是预期行为。然而,将 var 传递给 `extends?`(以及其他协议辅助函数)会抛出 NPEs,因为它们不会解引用 var。

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