我有一个函数,它生成一个称为a-function的惰性序列。
如果我运行代码:
(map a-function a-sequence-of-values)
它按预期返回一个惰性序列。
但当我运行代码时:
(mapcat a-function a-sequence-of-values)
它打破了我懒散的工作。事实上,它将代码转换为
(apply concat (map a-function a-sequence-of-values))
因此,在连接这些值之前,它需要实现映射中的所有值。
我需要的是一个函数,它根据需要连接映射函数的结果,而无需事先实现所有映射。
我可以破解一个功能:
(defn my-mapcat
[f coll]
(lazy-seq
(if (not-empty coll)
(concat
(f (first coll))
(my-mapcat f (rest coll))))))
但我不敢相信clojure没有做任何事情。你知道clojure是否有这样的功能吗?只有少数人和我有同样的问题?
我还发现了一个处理同样问题的博客:http://clojurian.blogspot.com.br/2012/11/beware-of-mapcat.html
懒惰序列的生产和消耗与懒惰评估不同
Clojure函数对其参数进行严格/热切的评估。对产生惰性序列的自变量的求值不会强制实现产生的惰性序列本身。但是,对论证的评价会产生任何副作用。
mapcat
的普通用例是连接产生的没有副作用的序列。因此,急切地评估一些论点并不重要,因为预计不会有副作用。
函数my-mapcat
通过将参数封装在thunks(其他惰性seq)中,在参数的求值上施加了额外的惰性。当预期会出现显著的副作用(IO、显著的内存消耗、状态更新)时,这可能很有用然而,如果你的函数产生了副作用,并产生了一个序列,你的代码可能需要重构,那么你的脑海中可能会响起警钟
这里是类似的算法。monads
(defn- flatten*
"Like #(apply concat %), but fully lazy: it evaluates each sublist
only when it is needed."
[ss]
(lazy-seq
(when-let [s (seq ss)]
(concat (first s) (flatten* (rest s))))))
写my-mapcat
的另一种方法:
(defn my-mapcat [f coll] (for [x coll, fx (f x)] fx))
将函数应用于延迟序列将强制实现该延迟序列的一部分,以满足函数的自变量。如果该函数本身产生了惰性序列,那么这些序列就不会理所当然地实现。
考虑此函数来计算序列的已实现部分
(defn count-realized [s]
(loop [s s, n 0]
(if (instance? clojure.lang.IPending s)
(if (and (realized? s) (seq s))
(recur (rest s) (inc n))
n)
(if (seq s)
(recur (rest s) (inc n))
n))))
现在让我们看看实现了什么
(let [seq-of-seqs (map range (list 1 2 3 4 5 6))
concat-seq (apply concat seq-of-seqs)]
(println "seq-of-seqs: " (count-realized seq-of-seqs))
(println "concat-seq: " (count-realized concat-seq))
(println "seqs-in-seq: " (mapv count-realized seq-of-seqs)))
;=> seq-of-seqs: 4
; concat-seq: 0
; seqs-in-seq: [0 0 0 0 0 0]
因此,实现了seqs的seq的4个元素,但没有实现其组成序列,也没有在级联序列中实现。
为什么是4?因为concat
的适用arity重载版本采用4个参数[x y & xs]
(计算&
)。
与比较
(let [seq-of-seqs (map range (list 1 2 3 4 5 6))
foo-seq (apply (fn foo [& more] more) seq-of-seqs)]
(println "seq-of-seqs: " (count-realized seq-of-seqs))
(println "seqs-in-seq: " (mapv count-realized seq-of-seqs)))
;=> seq-of-seqs: 2
; seqs-in-seq: [0 0 0 0 0 0]
(let [seq-of-seqs (map range (list 1 2 3 4 5 6))
foo-seq (apply (fn foo [a b c & more] more) seq-of-seqs)]
(println "seq-of-seqs: " (count-realized seq-of-seqs))
(println "seqs-in-seq: " (mapv count-realized seq-of-seqs)))
;=> seq-of-seqs: 5
; seqs-in-seq: [0 0 0 0 0 0]
Clojure有两种解决方案可以使参数的评估变得懒惰。
一个是宏。与函数不同,宏不计算其参数。
这是一个具有副作用的功能
(defn f [n] (println "foo!") (repeat n n))
即使未实现序列,也会产生副作用
user=> (def x (concat (f 1) (f 2)))
foo!
foo!
#'user/x
user=> (count-realized x)
0
Clojure有一个lazy-cat
宏来阻止此
user=> (def y (lazy-cat (f 1) (f 2)))
#'user/y
user=> (count-realized y)
0
user=> (dorun y)
foo!
foo!
nil
user=> (count-realized y)
3
user=> y
(1 2 2)
很遗憾,您无法apply
宏。
延迟评估的另一个解决方案是封装thunks,这正是您所做的。
您的前提是错误的。Concat是惰性的,如果它的第一个参数是,那么apply是惰性的;mapcat是惰性的。
user> (class (mapcat (fn [x y] (println x y) (list x y)) (range) (range)))
0 0
1 1
2 2
3 3
clojure.lang.LazySeq
请注意,一些初始值是经过评估的(下面将对此进行详细介绍),但很明显,整个过程仍然是惰性的(或者调用永远不会返回,(range)
返回一个无休止的序列,并且在频繁使用时不会返回)。
你链接到的博客是关于在懒惰树上递归使用mapcat的危险,因为它渴望最初的几个元素(这些元素可以在递归应用程序中相加)。