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

欢迎!请访问关于页面以了解更多关于如何操作的信息。

+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确实起作用,它通过遍历惰性序列并强制每个元素展开,然后返回头部。因此,它们的类没有不同,但内部发生了副作用以实现值。现在您已经有了实现的数据,而不再是头部悬空的未实现指针。

懒序列 str 的问题很不幸,还有一个与此相关的问题在这里/jira上。我怀疑最初没有打印懒序列的元素,因为懒序列可能是无限的,打印这些元素需要花费很长时间。

by
感谢快捷回应 - 这很有帮助。总结一下:
.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”操作(因为在那个时刻,整个包装过程将不再有任何用途,而且在我们需要保留集合时可以回收垃圾)。

好吧,我总是可以自己编写unwrap函数 - 或者也许出于这个原因已经有一个了?(那会很方便)

无论如何,我认为新技术入门者应该警告str/.toString与懒包装器的行为,以及doall不会展开任何内容的实际情况。请注意,文档指出
"...遍历序列的后续nexts,保留头部并返回它..."
这听起来像我们可能会得到一个没有“懒性”附加的链表的头部。像我这样的新手可能会按字面理解,最终可能会感到非常困惑和不安...
by
这基本上是真的,但是实现惰性序列和普通非惰性列表之间实际上并没有区别。一旦实现,这就是你得到的内容:一个没有惰性的链表头,这正是doall要达到的目的。但是由于效率,你得到的是你开始时相同列表的头。它只是不再惰性。

在doall中将所有内容复制到新列表节点中不会有任何好处,而是会带来时间和新分配的结构以及垃圾处理成本。如果你想真正承担这些成本(例如)你可以从惰性序列的头部创建一个向量。你上面给出的unwrap示例可以用这个来代替:(into [] items)或者甚至是(vec items)。

对于不习惯惰性计算的的人来说,确实无法回避这种情况会使人感到困惑和摸不着头脑。在未来,你还有更多奇怪的惊喜等着你去体验,直到它变成你的第二天性。

还需要考虑的另一件事是,通常你之所以使用doall,是因为在计算序列的惰性元素期间可能发生副作用,你需要这些副作用立即发生。否则你不在乎何时(如果有的话)解决惰性。但有许多问题是因为人们将网络操作等放在(for…)中,而没有做任何处理返回的惰性结果,然后奇怪为什么网络操作从未发生。最终你会意识到,像那种代码正确的结构是(doseq…),而不是(for…)。
...