请在 Clojure 2024 调查! 分享您的想法。

欢迎!有关此工作的更多信息,请参见 关于 页面。

+1
Clojure

这是一个注释过的示例函数,它打印出意外结果。
问题在于
为什么 doall 没有提供给我实际数据,使我无法从中生成字符串?
如果 doall 不起作用,我如何访问懒性序列下面的数据?
这是 1.11.1 版本中的 bug,还是我完全误解了 doall?
以下是演示代码

(defn lazytest []
(let [a [1 2 3 4] b [1 0 3 9] res (map #(if (= %1 %2) %1) a b)]

(println (str res))                

;; 这个惰性序列的 .toString 是类似 clojure.lang.LazySeq@e8c3d 的内容

(println (str (doall res))) 

;; doall 应该使其物化,但它仍然只返回了惰性序列!!

(println "seq is a " (class res))  

;; 这是一个 clojure.lang.LazySeq

(println "doall results in a " (class (doall res))) 

;; 这也是一个 clojure.lang.LazySeq!!!什么鬼?

(println res)      ;; this prints (1 nil 3 nil) which I would 
;; expect also from the second println above!
(println "are res and (doall res) identical? " (identical? res (doall res)))

;; 打印 true,这似乎是错误的/有 bug...
) )

1 答案

+3

doall 是可以工作的,它通过遍历懒性序列,迫使每个元素物化,然后返回头部。所以类并没有不同,但是它内部发生了副作用以实现值。现在你有了实现的数据,而不是头部的一个悬挂的未实现指针。

关于 LazySeq 的 str 是不幸的,这里也有与该问题相关的另一个问题/bug。我怀疑它最初没有打印 LazySeq 内容,因为懒性序列可以是无限的,打印这些内容要花费很长时间。

感谢快速的回复 - 这很有帮助。总结一下
.toString(正如预期)不会触发物化,它更适用于调试目的,而不是文本格式化(至少对于懒序列来说是这样)。

我的错误是期待doall返回一个纯集合(因为一切都是物化的),但它只是返回它的参数。为了保持一致性,.toString现在不会检查是否一切都已物化,但它仍然展现出不检查内部行为。

如果我将str应用于懒序列的元素,则会正常工作
(defn str-unwrap [items]
  (apply str (interpose " " items))
)
我添加了interpose以获取分隔符并忽略str将nil表示为空字符串。在大多数情况下,这不会对当前目的造成问题(在我的应用程序中,我通常会过滤掉所有nil元素)。

我也可以通过以下方式手动生成非懒序列,例如:
(defn unwrap [items]
  (reduce #(conj %1 %2) [] items)
)
我可以在进一步处理中使用这一点,并且-作为一种最简单的方式-可以调用(str (unwrap items))

这几乎相当于(print items),只是对于println items显示的括号代替了向量中的方括号 - 这不是真正的问题,因为所有的这些最终都将受到进一步处理的双重处理... 超过简单的只使用str。

我的错误是期待doall对懒包装器进行“解包”(因为在该时间点,整个包装过程将不再有用,并且我们可以当需要保持集合时进行垃圾回收)。

好吧,我也可以自己制作unwrap函数 - 或者可能已经有一个为这个原因存在的了?(很好)

无论如何,我认为新手应该警告关于str/.toString与懒包装器的行为以及doall不能解包任何事情的事实。请注意,文档中写道
"...遍历seq的连续nexts,保留头并返回它..."
这似乎意味着我们可能会得到一个不带“懒加载”的链表的头。新手像我可能会认真看待这一点,并且最终会非常困惑和紧张...
这基本上是对的,但实际上一旦物化了使用的懒序列和普通非懒序列之间没有真正差异。一旦它被物化,那正是你得到的:一个不带“懒加载”的链表的头,这正是doall的目的。但是你返回的是列表的头,为了效率。它只是不再懒了。

在doall中将所有内容复制到新的列表节点中,实际上并不会带来任何好处,反而会带来时间、新分配的结构和垃圾处理等成本。如果您确实愿意承担这些成本,可以(例如)从惰性序列的头部创建一个向量。您上面给出的unwrap例子可以替换为这个:直接使用(into [] items)或(vec items)。

对于不熟悉惰性求值的用户来说,确实很难避免因为惰性求值而造成的困惑。在未来,您可能还会遇到更多奇怪的情况,直到它成为您的本能。

还有一点需要考虑的是,通常只有在序列惰性元素的计算过程中发生副作用时,您才会使用doall,并且需要那些副作用立即发生。否则,您并不在乎惰性何时(如果有的话)解决。但是,由于人们将网络操作等放入(for ...)中,而没有对返回的惰性结果进行处理,从而导致了许多错误,他们不知道为什么网络操作从未运行。最终,您会意识到,像这样的代码应该使用(doseq ...)而不是(for ...)。
...