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是有效的,它通过遍历懒性序列,强制每个元素被具体化,然后返回头部。所以,类没有不同,但它内部发生了副作用以实现值。你现在有了实现数据,而不是悬而未决的未实现指针在头部。

关于LazySeq的 str 的事情很不幸,这里有另一个关于该问题的疑问/bug。我怀疑它最初没有打印懒性序列的内容,因为懒性序列可能是无限的,打印这些内容会花费永远。

感谢您的快速回复——这很有帮助。总结一下
.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)中显示的括号——这实际上并不成问题,因为所有这些都将在进一步的简单使用中 subjected to further processing...

我的错误在于期望doall对惰性包装器进行“展开”(因为在该时间点,包装一切将不再有用,我们只想保留集合时可以将其回收)。

好吧,我总是可以自己实现unwrap函数——或者也许有一个已经存在的函数?(那将是件好事)

无论如何,我认为新手应该被警告关于str/.toString与惰性包装器的行为,以及关于doall并不展开任何内容的事实。请注意,文档上写着
"...遍历序列的连续nexts,保留头部并返回它..."
这听起来像是我们可能会得到一个没有“惰性”的链表的头。像我这样的新手可能会字面地理解并最终感到困惑和紧张...
这大致是正确的,但实际上实现后的惰性序列和普通非惰性列表之间没有区别。一旦它被实现,那就是你得到的东西:没有惰性的链表的头,这正是doall的目的所在。但是你得到的是你开始时同一列表的头部,为了提高效率。但是它不再是惰性的。

在doall中将所有内容复制到新的列表节点中不会带来任何好处,反而会付出时间、新分配的结构和垃圾收集的代价。如果你确实想付出这些代价,你可以(例如)从懒序列的头部创建一个向量。你提供的unwrap示例可以用以下内容替换: (into [] items) 或者甚至是 (vec items)。

对于习惯不使用懒计算的读者来说,懒计算确实让人困惑。在你熟悉它之前,你可能会面临更多的意外。

还需要考虑的是,通常你使用doall的唯一原因是在计算序列的懒元素时会产生副作用,你需要这些副作用立即发生。否则你并不关心懒加载何时(如果有的话)解决。但许多人因为例如将网络操作放在(for ...)中,并且从未处理过返回的懒结果,然后 wondered why the network operations never took place,从而导致了许多bug。最终你会了解到,对于这类代码,正确的结构是(doseq ...),而不是(for ...)。
...