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

欢迎!请查看关于页面以了解更多关于其工作方式的信息。

+1
Clojure

错误CLJ-2094演示了从像partial这样的函数引起的协议边缘情况,这些函数会贪心求值它们的参数并因此保留过时的引用。我最近遇到了这个问题,发现这个请求被拒绝是因为“标准的Clojure求值规则”。

我认为有一种简单的协议修复方法,它增加的额外开销很小,并允许协议相关函数在贪心和懒求值上下文中工作。团队对这个修复方案感兴趣吗?

也许您可以提供一些关于“简单的修复方法”及其可能对现有代码产生的影响的更多信息,这样人们就可以更深入地了解修补程序可能涉及的内容?
同意Sean的看法,您能分享这个想法吗?

编辑
以前的看法通常是你们更希望我在推送补丁前先进行检查,因此我认为在把代码推到你们面前之前,先做一下幻象检查。

我已签署CLA

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

简而言之:在几个查看`:imps`的协议函数中引用协议上的var,以便查看任何更新。
在我的criterium基准测试中,每次调用的开销大约为+5纳秒。
如果我将调用移至最内层使用点,性能成本也会降低。

1 个回答

0

这个问题更像是一个提议的解决方案而非问题,这使得我难以将其作为工单记录。我不确定 CLJ-2094 是否是你真正关心的实际问题,还是其他方面。如果是其他方面,请分享。

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

但是,协议已经有了机制来在添加扩展到协议时重置其状态,因此我认为这个问题已经基本解决。只有当协议状态没有与当前状态匹配时,才有可能。在这种情况下,我认为原因是协议函数 var 包含一个函数实例,该实例有一个缓存,包含协议,但函数实例本身是旧的(var 包含新的函数实例)。

所以你使用的是一个无效函数,但在方法调用期间执行查找以恢复活动函数的等价状态。在调用路径中添加 var 查找似乎是行不通的,因为整个协议设计都在试图避免这一点(这就是我们为什么有一个在修改时可以丢弃的缓存)。这里的替代方案是将函数的 var 查找推向调用者。这正是 CLJ-2094 中对于匿名函数实现规范(与部分函数相比)所获得的内容。

再次提醒,我不确定我们实际上想要解决什么问题。在 CLJ-2094 中,问题是函数被部分函数捕获。解决方案是 - 不要捕获值。我们还可以考虑某些替代的方案来保留 var 查找 - 你可以想象一个 partial-var,它接受一个 var 而不是从符号中评估的函数。一种权宜之计是使用 wrap 在函数中延迟评估。这算是一个足够的问题,需要新的解决方案吗?我不知道。

by
感谢你对情况的详细描述,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

> 求解方案——不要捕获值。

显然,这是当前解决方案,并且在许多其他场景(如函数自身)中都有效。我认为这就是不一致性导致的不必要痛苦的地方,并且可以得到积极的改进。

您说过,由于已经投入了方法缓存的努力,因此无法委托变量。您是否担心我的建议的更改会被用于生成的函数或类似的内容?我的建议的更改仅影响协议辅助函数(`extends?`、`extenders`、`find-protocol-method`和`satisfies?`),并且不与基本协议方法构造函数交互,因此现有的方法缓存将继续按当前方式工作。

再次感谢您抽出时间撰写详细的评论。我真的很珍视您在这些讨论中付出的努力。
by
> 我试图解决的问题是将死亡协议函数的使用与活协议函数的使用保持一致。

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

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

> "not"吗?我认为是“are”,因为副作用

> 这可能导致在定义协议的命名空间和扩展它们的其他命名空间之间出现微妙错误,如果协议在调用后续命名空间之前被评估。

> 我认为这实际上开始触及问题所在,这实际上是REPL进程问题。让我们尝试明确地说:有时候将目标分解为目标和障碍是有帮助的。

> 目标:在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`,则会返回 `false` 并引发 "未实现" 异常的 `partial-extends` 和 `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` 上的 deref'ing 将比当前代码路径慢。
* 此问题实际上是参考文献与变量(请参阅 `(def fns {:key1 some-func :key2 other-func})` 与 `(def fns {:key1 #"some-func" :key2 #"other-func"})`) 的更一般问题。
* `extend-protocol` 的全局状态实际上并不是全局的(与 spec 的全局注册表不同)。
* `partial` 实际上是一个糟糕的函数,应该避免。

看起来如何?
我不同意这是一个理想的目标(尽管这与协议无关)。 :)
如果您不介意,您认为这是一个期望的目标吗?





```
(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
```



```
(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)
执行错误(NumberFormatException)在user/eval189(REPL:1)。
```


...