【问题标题】:Removing elements from lazy sequences in a large Clojure tree structure, avoiding head retention从大型 Clojure 树结构中的惰性序列中删除元素,避免头部保留
【发布时间】:2015-09-19 11:40:30
【问题描述】:

问题描述

对于在 Clojure 中处理大型数据结构,惰性序列提供了一种很好的惯用方法。一个人需要小心避免头部 不过保留。

我很难处理这样的大树结构:

                 R                                         Root
       __________|____________________
       A                   B         C, D, E, ...          1st Level Children
_______|_______     _______|_______
X Y Y ... Y X Y     X Y Y ... Y X Y                        2nd Level Children
  • 所有节点都是带有键 :content 的映射。任何:content 的值都是包含该节点所有子节点的惰性序列。
  • 整棵树不适合内存。第 2 层的 Y 项目太多。
  • 不包括Y 项的整个树都适合内存。

处理完树后,我想得到一棵新树,其中所有Y 节点都被删除:

           R
     ______|__________________
     A             B         C, D, E, ...
_____|___     _____|___
X X ... X     X X ... X

示例代码及进一步说明

;; Generating example data
;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn root [content]
  {:tag :root :content content})

(defn lazy-elements [n tag content]
  (lazy-seq (repeat n {:tag tag :content content})))

(defn level-1 [content]
  (lazy-elements 3 :A content))

(defn level-2 [n]
  (concat (lazy-elements 10 :X '(:leaf))
          (lazy-elements n :Y '(:leaf))))

(defn remove-nodes [node]
  (remove #(= (:tag %) :Y) node))


;; Illustrating usage
;;;;;;;;;;;;;;;;;;;;;

;; runs and runs and runs... and eventually returns correctly
(defn valid-run []
  (->> (root (level-1 (level-2 1e8)))
       :content
       first
       :content
       remove-nodes))

;; Does not terminate properly, runs out of memory
(defn invalid-run []
  (->> (root (level-1 (level-2 1e8)))
       :content
       (map :content)       ; source of head retention
       (map remove-nodes)))

(Gist available on GitHub)

第二个示例将崩溃(根据可用内存,可能需要调整 2 级项目的数量)。映射在 :content 的所有 1 级项目引入了一个参考,该参考在循环浏览所有内容项目时会导致头部保持问题 删除不需要的 :Y 项目。

我能够使用来自valid-run 之类的数据,将其置于保持可变状态的 var 中,为所有相关节点执行此操作 然后再次将所有数据拼接在一起。但我对这种方法非常不满意,因为必须依赖可变性 最后必须使用一些非常命令式的代码来合并数据(例如,通过列表的索引运行)。

问题

如何以函数式、声明式的风格实现这一点?理想情况下,我希望避免使用可变状态以及 过于迫切(例如,使用索引等将集合拼接在一起)。

资源

以下文章和 sn-ps 是关于该问题各个方面的有趣读物:

更多背景知识

最终我需要它来处理大型 XML 文件。大意味着> 1GB并且将其解析成树将不适用于可用 记忆。从那个 XML,我想将一些元素放入存储桶 A(假设是一个数据库表),并将 XML 树的所有其余部分放入存储桶 B. 提取的子树当然应该保留 XML 结构。

除了将 XML 解析为树之外,我还可以将 XML 处理为事件流,例如通过data.xml/source-seq。然而, 这将意味着失去 XML 树语义。会工作,但不是很漂亮。但也许还有其他处理方法 首先是 XML。

【问题讨论】:

  • 您正在构建一棵新树。你能在创作的时候把它写出来,然后释放你已经写的东西吗?在第 i 层,您有一个惰性节点序列。选择下一个节点,然后进入它。一旦你完成了那个节点,写出它的替换树并释放它和它下面的所有树。降序意味着迭代该过程,除了写作部分。这个想法是仅将您正在检查的子树保存在内存中,并写出并忘记您已经处理过的内容,懒惰地将新的子树带入内存。不确定这是否一切正常。
  • 另一个:你只从叶节点中清除 Y,对吧?在同一级别?如果是,则构造一个叶节点序列序列,代表同一节点下的叶节点。这些应该有标识符,以便您可以在剥离 Y 后将它们附加回它们的父节点。即:您首先遍历树只是为了构建叶节点序列的序列。然后你删除Y。然后您再次遍历树以创建具有新叶节点集的新树。写出子树以防止新树全部在内存中。同样,不确定...
  • 我的建议可能是可怕的想法。就在我的脑海中。
  • 我不得不说我还没有完全理解这个问题,但是我使用惰性拉链方法在 clojure 中的树上取得了成功,参考:josf.info/blog/2014/04/14/seqs-of-clojure-zippersjosf.info/blog/2014/04/14/seqs-of-clojure-zippers。这些使得处理树非常“实用”且不可变。
  • 我只想指出这是一个很好的问题。一个有趣的技术问题,通过背景、示例甚至图表进行了很好的解释;和可运行的代码 sn-ps 来重现确切的问题。它唯一缺少的是一个示例堆栈跟踪,因此没有人必须在本地运行它才能看到发生的错误。

标签: xml clojure tree lazy-evaluation


【解决方案1】:

问题是您的level-2 节点都有指向同一个内存中惰性序列的指针,然后您多次映射该序列。如果您只是让valid-run 同时处理第一个和第二个节点,您将遇到同样的问题 - 节点的数量并不重要,因为您使用任何两个节点都会破坏堆。在实际应用程序中,您从数据库或文件或其他任何内容中读取这些节点,它们将指向不同的对象,您可以依次懒惰地处理这些对象。

如果您生成更具代表性的示例数据(即相同的数据但没有结构共享),您可以在处理时对每个节点进行 GC:

(defn root' [content]
  (fn []
    {:tag :root :content (content)}))

(defn lazy-elements' [n tag content]
  (repeatedly n (fn [] {:tag tag :content (content)})))

(defn level-1' [content]
  (fn []
    (lazy-elements' 3 :A content)))

(defn level-2' [n]
  (fn []
    (concat (lazy-elements' 10 :X (fn [] '(:leaf)))
            (lazy-elements' n :Y (fn [] '(:leaf))))))

(defn remove-nodes [node]
  (remove #(= (:tag %) :Y) node))

(defn run []
  (let [root-builder (root' (level-1' (level-2' 1e8)))]
    (->> (root-builder)
         :content
         (map :content)       
         (map remove-nodes))))

user> (pprint (run))
(({:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)})
 ({:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)})
 ({:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}
  {:tag :X, :content (:leaf)}))

由于我们只是生成示例内容,我已经调整了所有节点构建器,而不是他们应该存储 N 个副本的对象,而是他们应该调用 N 次以获得 N 个不同对象的函数。它们不是返回一个节点,而是返回一个函数,当调用该函数时,会生成该节点的副本;这使它们可以像您的原始版本一样完美地组合,只需要在外层进行一个额外的函数调用。如果您实际上已经有不同的对象,就像我怀疑您在实际应用程序中那样,您可以直接使用您编写的原始函数。

【讨论】:

  • 你当然是对的。生成的示例数据并不反映我正在使用的实际数据。用真实数据来测试你提出的建议,然后再回来——理想情况下只是接受你的答案。 ;)
  • 谢谢,这确实按预期工作。为了答案的完整性,也许您可​​以添加一些代码来实际重建完整的树。我通过(defn run [size] (let [root-builder (root' (level-1' (level-2' size))) update-contents (fn [f node] (update-in node [:content] f))] (-> (root-builder) (update-in [:content] #(map (partial update-contents remove-nodes) %))))) 对此进行了测试,我将调查为什么这不适用于我获得的 XML 数据。可能是 data.xml 添加到数据结构中的东西。
  • 因此,您可能遇到的新问题是,对于我所知道的任何惰性模型,读取 XML 流并获得“惰性树”非常困难。很难真正看到 c.d.xml 内部发生了什么:它 声称 是懒惰的,这可能取决于“懒惰”的含义,但我建议仔细检查一下。由于您有一个很大的 XML 文件,请尝试使用 (defn walk [tree] (when (coll? tree) (doseq [x tree] (walk x)))) 遍历整个内容,看看是否会耗尽您的内存。
  • @nblumoe 我刚刚下载了一个 700MB 的 xml 文件,并尝试了 (walk (xml/parse (reader the-file)))。如果这实际上是一棵惰性树,它将在恒定空间中运行,并最终输出 nil。相反,它消耗了我系统上的所有内存,将整个树固定在适当的位置。如果您想延迟处理 XML,您可能必须自己处理 SAX 事件流,而不是让其他人为您将其整理成树。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-12-08
  • 1970-01-01
  • 2013-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多