从大型 Clojure 树结构中的惰性序列中删除元素,避免头部保留
Removing elements from lazy sequences in a large Clojure tree structure, avoiding head retention
问题描述
对于在 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)))
第二个示例会崩溃(根据可用内存,可能需要调整 2 级项目的数量)。映射在
:content
的所有级别 1 项目引入了一个引用,该引用会在循环遍历所有内容项目时导致头部保留问题
删除不需要的 :Y
项。
我能够使用来自 valid-run
之类的数据,将其放入保持 mutable 状态的 var 中,对所有相关节点执行此操作
然后再次将所有数据拼接在一起。但我对这种方法非常不满意,因为它必须依赖于可变性
并且最后必须使用一些非常必要的代码来合并数据(例如 运行 通过列表索引)。
问题
如何以函数式、声明式的方式实现这一点?理想情况下,我希望避免使用 mutable 状态以及
过于迫切(例如,使用索引等将集合拼接在一起)。
资源
以下文章和片段是关于该问题各个方面的有趣读物:
- XML manipulation in Clojure
- Rich Hickey on incidental complexity of head retention
- Related Clojure documentation
- Head retention funkiness in a nutshell
更多背景
最终我需要它来处理大型 XML 文件。大意味着 >1GB 并且将其解析为树将不适用于可用的
记忆。从那个 XML 我想将一些元素放入桶 A(假设是一个数据库 table)并将 XML 树的所有其余部分放入桶中
B、提取出来的子树当然要保留XML结构
除了将 XML 解析成树,我还可以将 XML 作为事件流处理,例如通过 data.xml/source-seq。然而,
这将意味着失去 XML 树语义。会工作,但不漂亮。但也许还有其他方法可以处理
XML 第一名。
问题是您的 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 个不同对象的函数。而不是 returning 一个节点,他们 return 一个函数,当被调用时,生成该节点的副本;这使它们可以像您的原始版本一样好地组合,只需要在外层调用一个额外的函数。如果您实际上已经有不同的对象,就像我怀疑您在实际应用程序中那样,您可以只使用编写的原始函数。
问题描述
对于在 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)))
第二个示例会崩溃(根据可用内存,可能需要调整 2 级项目的数量)。映射在
:content
的所有级别 1 项目引入了一个引用,该引用会在循环遍历所有内容项目时导致头部保留问题
删除不需要的 :Y
项。
我能够使用来自 valid-run
之类的数据,将其放入保持 mutable 状态的 var 中,对所有相关节点执行此操作
然后再次将所有数据拼接在一起。但我对这种方法非常不满意,因为它必须依赖于可变性
并且最后必须使用一些非常必要的代码来合并数据(例如 运行 通过列表索引)。
问题
如何以函数式、声明式的方式实现这一点?理想情况下,我希望避免使用 mutable 状态以及 过于迫切(例如,使用索引等将集合拼接在一起)。
资源
以下文章和片段是关于该问题各个方面的有趣读物:
- XML manipulation in Clojure
- Rich Hickey on incidental complexity of head retention
- Related Clojure documentation
- Head retention funkiness in a nutshell
更多背景
最终我需要它来处理大型 XML 文件。大意味着 >1GB 并且将其解析为树将不适用于可用的 记忆。从那个 XML 我想将一些元素放入桶 A(假设是一个数据库 table)并将 XML 树的所有其余部分放入桶中 B、提取出来的子树当然要保留XML结构
除了将 XML 解析成树,我还可以将 XML 作为事件流处理,例如通过 data.xml/source-seq。然而, 这将意味着失去 XML 树语义。会工作,但不漂亮。但也许还有其他方法可以处理 XML 第一名。
问题是您的 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 个不同对象的函数。而不是 returning 一个节点,他们 return 一个函数,当被调用时,生成该节点的副本;这使它们可以像您的原始版本一样好地组合,只需要在外层调用一个额外的函数。如果您实际上已经有不同的对象,就像我怀疑您在实际应用程序中那样,您可以只使用编写的原始函数。