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

欢迎!请查看关于页面以了解有关这如何工作的更多信息。

+1 投票
Clojure

这里是一个打印出预期结果的示例函数。
问题在于
为什么doall不提供实际数据,以便我可以从它们生成一个字符串?
如果doall不起作用,我如何获取延迟序列下隐藏的数据?
这是1.11.1版本中的错误,还是我对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!!!WTF?

(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的问题很不幸,关于这个问题还有一个在此处/Issue的另一个问题。我怀疑它最初没有打印LazySeq的内容,因为LazySeq可能是无限的,打印这些内容需要很长时间。

谢谢快速的回复 —— 这非常有帮助。为了总结一下
.toString将(如预期)不会触发具体化,它更多的是用于调试目的,而不是用于文本格式化(至少对于lazy seqs而言)。

我的错误在于预期doall会返回一个普通的集合(因为一切都已经被具体化了),但它只是返回其参数。而且,为了保持一致性,.toString现在不再检查是否一切都已具体化,但仍表现出不查看内部的行为。

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

我也可以通过手动生成非lazy集合,例如
(defn unwrap [items]
  (reduce #(conj %1 %2) [] items)
)
我可以用这个来进行进一步处理,并且最简单的方式是调用(str (unwrap items))

这几乎与(print items)相同,只是在vector中的方括号而不是(println items)中显示的括号 - 这不是真正的问题,因为所有这些都将受到进一步的处理……超越了仅仅使用str的简单用途。

我的错误是预期doall会执行一个“unwrap”操作来解除lazy包装器(因为在这个时候,整个包装操作将不再有任何用处,并且在我们只想要集合时可以随时垃圾回收)。

好吧,我总是可以自己实现unwrap函数——或者也许早已存在了一个为此目的而创建的函数?(那会很棒)

无论如何,我认为新手应该被警告关于str/.toString与lazy包装器的行为,以及doall实际上并不解除任何内容的现实。
注意,文档说
“……遍历seq的连续nexts,保留头部并返回它……”
这听起来像我们可能会得到一个没有任何“laziness”附加的链表头部。像我这样的新手可能会字面理解,并最终感到非常困惑和紧张……
这基本上是对的,但是实现惰性序列和普通非惰性序列之间确实没有区别。一旦它实现,这就是你得到的东西:没有惰性的链表头,这正是doall的目的。但是,为了效率,你会得到你开始的相同列表的头。它只是不再惰性。

在doall中复制一切到新的列表节点没有好处,而且会有时间、新分配的结构和垃圾收集的成本。如果你真的想要承担这些成本,你可以(例如)从惰性序列的头创建一个向量。你上面提到的unwrap示例可以替换为: (into [] items) 或者 (vec items)。

对不熟悉惰性评估的人来说,这是一个让人困惑和混淆的事实。在你适应它之前,你的未来会有很多奇怪的惊喜。

需要考虑的另一件事是,通常你使用doall的唯一原因是如果序列的惰性元素的计算过程中发生副作用,你需要现在就看到这些副作用。否则,你不关心惰性何时解决(如果有的话)。但有许多限制是由人们造成的,例如,将网络操作放在(for ...) 中,从不处理返回的惰性结果,然后纳闷为什么网络操作从未发生。最终你会学到,类似的代码的正确构造是 (doseq ...),而不是 (for ...)。
...