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 之间的关键区别在于,前者中 lazy-seq 实际上将其整个主体包装在一个带有 ^:once 的函数中。

之所以这样做的原因是在上面的代码中,内部的 primes 是一个闭包值,它成为一个局部变量。当 lazy-seq 的主体函数(带有 ^:once)被第两次调用时,其局部变量是 nil,因此内部 primes 也是 nil。然后迭代停止。
在 CLJS 中,没有 :once 也没有局部变量清除,因此内部 primes 从不为 nil,并且执行会继续进入该 fn 而不调用那个 zero?

即使这种推理听起来不太合理,但关于正是 :once 导致差异的假设可以很容易地通过在 CLJ 中不使用 :once 实现 lazy-seq 并注意到它会导致 StackOverflowError 来证实。

我不知道在这种情况下该怎么办,但或许至少应该在 https://script.clojure.org/about/differences 页面上增加一条注释。

1 答案

0

Slack讨论中链接的博客文章在描述这个工作原理时是错误的。

我无法就cljs方面的内容发表意见,但在clj方面,这里的expression中primes从来不会被闭包,指向vars(全局变量)的名字的值也没被闭包。

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`的类的`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`的相同`LazySeq`进行迭代将不会产生任何东西。
...