如何理解clojure的lazy-seq

时间:2017-05-21 09:48:13

标签: recursion lambda clojure lisp lazy-evaluation

我试图了解clojure的lazy-seq运算符,以及懒惰评估的概念。我知道这个概念背后的基本思想:表达式的评估会延迟到需要的值。

一般来说,这可以通过两种方式实现:

  • 在编译时使用宏或特殊形式;
  • 在运行时使用lambda函数

使用惰性评估技术,可以构建被评估为已消耗的无限数据结构。这些无限序列利用lambdas,闭包和递归。在clojure中,使用lazy-seqcons形式生成这些无限数据结构。

我想了解lazy-seq如何做到这一点。我知道它实际上是一个宏。请考虑以下示例。

(defn rep [n]
  (lazy-seq (cons n (rep n))))

这里,rep函数返回一个延迟评估的类型LazySeq的序列,现在可以使用序列API对其进行转换和使用(因此进行评估)。此API提供的功能takemapfilterreduce

在扩展形式中,我们可以看到lambda如何用于存储单元格的配方而不立即进行评估。

(defn rep [n]
  (new clojure.lang.LazySeq (fn* [] (cons n (rep n))))) 
  • 序列API如何与LazySeq 一起使用?
  • 以下表达式实际发生了什么?

(reduce + (take 3 (map inc (rep 5))))

  • 如何将中间操作map应用于序列
  • take如何限制序列
  • 终端操作reduce如何评估序列

此外,这些功能如何与VectorLazySeq 配合使用?

此外,是否可以生成嵌套的无限数据结构 ?: list包含列表,包含列表,包含列表......无限宽和深,评估为序列API消耗?< / p>

最后一个问题,这个

之间有什么实际区别
(defn rep [n]
  (lazy-seq (cons n (rep n))))

(defn rep [n]
  (cons n (lazy-seq (rep n))))

1 个答案:

答案 0 :(得分:13)

这是很多问题!

seq API如何与LazySeq实际配合使用?

如果您查看LazySeq的类源代码,您会发现它实现了ISeq接口,提供了firstmore和{{1}等方法}。

nextmaptake等功能是使用filter(它们产生延迟序列)和lazy-seq以及first(其中反过来使用rest),这就是他们如何使用lazy seq作为输入集合 - 使用more类的firstmore实现。

以下表达式实际发生了什么?

LazySeq

关键是要了解(reduce + (take 3 (map inc (rep 5))))的工作原理。它将调用包装函数来获取和记忆结果。在您的情况下,它将是以下代码:

LazySeq.first

因此,它将是(cons n (rep n))作为其值的cons单元格和另一个n实例(递归调用LazySeq的结果)作为其rep部分。它将成为此rest对象的实现值,LazySeq将返回缓存的cons单元格的值。

当您在其上调用first时,它将以相同的方式确保实现特定more对象的值(或重用已记忆的值)并在其上调用LazySeq (在这种情况下,在包含另一个more对象的cons单元格上more

获得LazySeq LazySeq对象的另一个实例后,当您在其上调用more时,故事会重复出现。

firstmap将创建另一个take,它将调用作为参数传递的集合的lazy-seqfirst(只是另一个懒惰的seq)这将是类似的故事。不同之处仅在于如何生成传递给more的值(例如,将cons调用为ffirst上映射的值LazySeq调用的值map 1}}而不是n函数中的rep原始值。

使用reduce它会更简单,因为它将使用loopfirstmore迭代输入延迟seq并应用reduce函数来生成final结果

正如maptake的实际实现一样,我建议您查看其源代码 - 这很容易理解。

seq API如何使用不同的集合类型(例如lazy seq和persistent vector)?

如上所述,maptake和其他功能在firstrest方面有效(提醒 - restmore之上实现{1}})。因此,我们需要解释firstrest / more如何使用不同的集合类型:它们检查集合是否实现ISeq(然后直接实现这些函数)或者他们尝试创建集合的seq视图,并将其firstmore的实现与其结合起来。

是否可以生成嵌套的无限数据结构?

这绝对有可能,但我不确定您想要获得的确切数据形状。你的意思是得到一个懒惰的seq,它会生成另一个序列作为它的值(而不是n中的rep之类的单个值)但是将它作为一个平坦的序列返回?

(defn nested-cons [n]
  (lazy-seq (cons (repeat n n) (nested-cons (inc n)))))

(take 3 (nested-cons 1))

;; => ((1) (2 2) (3 3 3))

宁愿返回(1 2 2 3 3 3)

对于这种情况,您可以使用concat而不是cons来创建两个或更多序列的延迟序列:

(defn nested-concat [n]
  (lazy-seq (concat (repeat n n) (nested-concat (inc n)))))

(take 6 (nested-concat 1))

;; => (1 2 2 3 3 3)

与此

有任何实际区别
(defn rep [n]
  (lazy-seq (cons n (rep n))))

这个?

(defn rep [n]
  (cons n (lazy-seq (rep n))))

在这种特殊情况下并非如此。但是在cons单元格没有包装原始值而是函数调用的结果来计算它的情况下,后一种形式不是完全懒惰的。例如:

(defn calculate-sth [n]
  (println "Calculating" n)
  n)

(defn rep1 [n]
  (lazy-seq (cons (calculate-sth n) (rep1 (inc n)))))

(defn rep2 [n]
  (cons (calculate-sth n) (lazy-seq (rep2 (inc n)))))

(take 0 (rep1 1))
;; => ()

(take 0 (rep2 1))
;; Prints: Calculating 1
;; => ()

因此即使你可能不需要它,后一种形式也会评估它的第一个元素。