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

欢迎!请查看 关于 页面了解更多相关信息。

0
ClojureScript

关于这个话题的起源,可以在 Slack 上找到相关讨论 https://clojurians.slack.com/archives/C03S1L9DN/p1650480331191719

这段代码在 CLJ 中运行良好,但在 CLJS 中会触发 StackOverflowError

(def primes (remove
             (fn [x]
               (some #(zero? (mod x %)) primes))
             (iterate inc 2)))

CLJ 和 CLJS 之间的关键区别在于,在 CLJ 中,lazy-seq 最终会将整个主体封装在一个带有 ^:once 的函数中。

这样做的推理是因为在代码中,内部的 primes 是一个封闭的值,它变成了局部变量。当 lazy-seq 的主体函数(带有 ^:once)第二次被调用时,其局部变量为 nil,因此内部 primes 也为 nil。迭代停止。
在 CLJS 中,没有 :once 和局部变量清除,所以内部 primes 永远不会是 nil,执行会不断进入那个 fn 而不调用那个 zero?

即使推理听起来不太可信,可以很容易地通过在 CLJ 中实现不带 :oncelazy-seq 并注意到这会导致 StackOverflowError 来证实这种差异是由于 :once 引起的假设。

我对此不确定要做什么,但也许至少应该在 https://script.clojure.org/about/differences 页面上添加关于它的注释。

1 个答案

0

Slack 讨论中提到的博文关于这一点是完全错误的。

我无法评论关于cljs方面的内容,但在这个表达式中,primes从未被封闭,引用变量(全局变量)的名称没有其值的封闭。

primes始终指代正在生成的lazy,从不指代nil。这是为什么这个可以工作,是因为某些函数的短路性质,以及任何可能作为数字除数的值总是出现在该序列中的数字之前。

https://clojurians.slack.com/archives/C03S1L9DN/p1651188211599599?thread_ts=1650481531.294919&cid=C03S1L9DN

那么,排除`:once`后行为的变化该如何解释呢?
因此,我决定深入挖掘,并写了`:once true`和`:once false`的版本,进行了编译和反编译,将其转换成可读的、某种程度上极简的Java代码,并测试以确定行为是否相同。这两个Java版本之间的唯一区别是`:once true`版本会在将`fn*`传递到`LazySeq`的`Class`中表示的`this.coll`上清除,而`:once false`版本则不会清除(同样,对于`pred`也是这样,但我已经去掉了它,以简化代码,因为它不影响任何东西)。

如果有人想尝试代码,可以这样
- 基于`:once true` - https://pastebin.com/H3ZbUK9m
- 基于`:once false` - https://pastebin.com/9FZLtsQm

如您所说,基于编译的/反编译的代码,我现在可以清楚地看到`primes`变量本身与任何东西都没有关系。但在`:once true`版本中,迭代在需要时停止,并非因为算法的具体性,也不是因为短路`some`(毕竟,它不会在素数上短路 - 它会耗尽`coll`),而是因为`FilterLazySeqFn`的实例本身停止可迭代 - 它的`coll`变为`null`。在此时的`LazySeq`处于一种悬浮状态 - 值已经被产生,但在`LazySeq.sval`中的`this.sv`上还没有被分配,因此在CLJ中绑定到`primes`的迭代将不会产生任何结果。
...