Clojure 打破懒散的地图猫

Clojure 打破懒散的地图猫,clojure,concat,lazy-sequences,Clojure,Concat,Lazy Sequences,我有一个生成惰性序列的函数,叫做a函数 如果我运行代码: (map a-function a-sequence-of-values) 它按预期返回一个惰性序列 但当我运行代码时: (mapcat a-function a-sequence-of-values) 它打破了我的懒散功能。事实上,它将代码转换为 (apply concat (map a-function a-sequence-of-values)) 因此,它需要在连接这些值之前实现映射中的所有值 我需要的是一个函数,它可以

我有一个生成惰性序列的函数,叫做a函数

如果我运行代码:

(map a-function a-sequence-of-values) 
它按预期返回一个惰性序列

但当我运行代码时:

(mapcat a-function a-sequence-of-values) 
它打破了我的懒散功能。事实上,它将代码转换为

(apply concat (map a-function a-sequence-of-values)) 
因此,它需要在连接这些值之前实现映射中的所有值

我需要的是一个函数,它可以根据需要连接map函数的结果,而不必事先实现所有map

我可以为此破解一个函数:

(defn my-mapcat
  [f coll]
  (lazy-seq
   (if (not-empty coll)
     (concat
      (f (first coll))
      (my-mapcat f (rest coll))))))
但我不敢相信clojure还没有做过什么。你知道clojure是否有这样的特征吗?只有少数人和我有同样的问题


我还发现了一个处理同样问题的博客:

你的前提是错误的。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的危险,因为它对前几个元素(可以在递归应用程序中加起来)很热心。

惰性序列的产生和消耗与惰性评估不同。

Clojure函数对其参数进行严格/急切的求值。对产生惰性序列的参数的求值不会强制实现产生的惰性序列本身。然而,任何由参数评估引起的副作用都会发生

mapcat
的一般用例是连接产生的序列而不产生副作用。因此,对某些论点进行急切的评估并不重要,因为预计不会有副作用

您的函数
mymapcat
通过将参数包装成thunk(其他惰性序列)来对其参数的计算施加额外的惰性。这在预期会产生显著的副作用(IO、显著的内存消耗、状态更新)时非常有用。然而,如果你的函数产生了副作用,并且产生了一个序列,你的代码可能需要重构,那么你的大脑中可能会响起警钟

这里是类似于algo.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?因为适用的arity重载版本的
concat
使用4个参数
[xy&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)
不幸的是,您无法应用宏


延迟评估的另一个解决方案是thunks包装,这正是您所做的。

还需要注意的是,
apply
是懒惰的,这取决于它的第一个参数是否懒惰。我不会说
apply
是懒惰的。相反,它是严格的。当然,它不会改变所应用函数的语义。换句话说,
apply
在调用
apply
本身时肯定会调用它的第一个参数。而map也总是完全计算它的第一个参数,我认为“lazy if the first argument is lazy”在这个问题的上下文中是正确的,
apply
是lazy。这并不意味着
(应用concat args)
在应用到
concat
之前必须完全实现序列
args
。举例来说,这将返回
nil
(apply(fn[&args])(range))
(下面将对此进行详细说明):我认为这是指A.Webb的答案,目前它在您的答案上方浮动。