Clojure哈希映射的懒惰性是否有意义?

3
我需要从我的函数中返回一个序列、一个数字和一个哈希映射(全部包装在向量中),以便打印的返回值看起来像这样:
[ ([:c :a] [:e :c] [:f :e] [:d :e] [:g :f] [:b :a])  15
  {:g :c, :f :a, :c :e, :d :a, :b :a, :c :a} ]

由于我的输入可能很大,我希望从函数返回惰性序列/对象。构建序列的对组(返回向量中的第一个对象)已经足够简单,只需将 'lazy-seq' 用于包装构建它的 'conj' 调用即可使其变为惰性。
哈希映射表(返回向量中的第三个对象,可能非常大)正在与序列相同的 loop-recur 块中进行构建(使用 assoc 调用)。哈希映射表是一些调用方将使用的附加信息,但如果对偶序列作为惰性返回,则我想知道是否有意义发送一个可能巨大的哈希映射表与(高效的)惰性序列一起返回,即使我将其作为可选的返回值。哈希映射表中的条目与惰性序列中的对组相关联。
因此,这是我的新手问题:在大型 HashMap 的位置上是否有任何意义发送 MapEntry 的惰性序列?也就是说,假设用户会取一块惰性序列的 MapEntry 并将它们转换为 hashmap 来进行查找... 如果失败,则他们将取下一个块,以此类推。这样惰性使用关联数据是否合理?
在 Clojure 中,有一些传统方法来返回/管理大的关联数据吗?感谢您提前给出的帮助和任何想法。
3个回答

7
不,无法提供一个惰性的映射。可以提供一个惰性的MapEntries序列,但是并不是很有用。还有一些其他类似的选项可能是合理的。
- 如果你说调用者可能根本不需要地图:那么返回一个地图的延迟,如果他们需要它,他们可以强制执行。 - 如果键很容易计算,但值很昂贵,则可以返回具有正确键和每个延迟值的完整映射;调用者只能强制执行他们需要的值。 - 仍然可以返回向量的惰性序列(我不会费力制作它们的MapEntries),但基本上没有办法让调用者将其视为惰性映射。他们要么只想查找已知的固定集合键(在这种情况下,他们将只惰性过滤条目,从不使其成为映射),要么想随意查找条目,在这种情况下,他们必须在查找第一个条目后将所有条目保留在内存中,以便仍然可以查找第二个,因此他们可能会将整个内容转储到完全实现的映射中。

非常感谢您的想法。键和值都很便宜(都是关键字);只是可能有很多。要返回的哈希映射基本上包含从一个指向另一个的指针。我对您的延迟想法很感兴趣。您能否详细说明如何为在loop-recur块中逐步构建的Map构建延迟呢? 您是指这个吗: (loop [ my-map {} my-pairs [] ...] ... (recur (delay (assoc my-map k v)) ...)) - Don
你不能(或者至少我并不建议)逐步构建地图延迟。只需延迟整个地图的构建:如果他们强制执行,他们就会得到整个地图。 (delay (loop [my-map {}] (if (...) (recur ...) my-map))) - amalloy
好的。这可能有点难以实现,因为我在同一个loop-recur块中构建我的lazy seq。但你肯定给了我一些可以使用的东西。 - Don

2
不,Clojure没有惰性映射。
另外,如果您正在使用loop/recur构建序列,则我认为尝试使其变成惰性不会有任何作用(除非生成每个元素很慢)。
看看这两个函数:
(defn bad-lazy-range [begin end]
  (loop [i (dec end) lst nil]
    (if (>= i begin)
      (recur (dec i) (lazy-seq (cons i lst)))
      lst)))

(defn good-lazy-range [begin end]
  (if (>= begin end)
    nil
    (lazy-seq (cons begin (good-lazy-range (inc begin) end)))))
bad-lazy-range会重复执行begin-end次,每次生成一个惰性序列链接的thunk(一种惰性计算),然后返回最外层的thunk。这个thunk需要保留对下一个thunk的引用,而下一个thunk需要对第三个thunk进行引用等等。你需要立即完成所有工作,并生成一个伪链接的thunk列表,它占用的空间比普通列表多。

然而,good-lazy-range会立即返回而不再递归--递归调用被隐藏在thunk中,并且直到必要时才会被评估。这也防止了堆栈溢出异常--如果没有lazy-seq调用,它可能会生成堆栈溢出异常,但在每个步骤中,它只评估一次对good-lazy-range的调用并返回。此时,调用者可以评估下一个调用,但此时第一个调用的堆栈帧已经消失了。

通常,只有在可以将其包装在大量计算中时才使用lazy-seq。在第一个函数中,它只包装了对cons的调用,而这个调用本来就会很快返回。然而,在第二个函数中,它包装了对cons和递归调用的调用,这意味着它正在延迟一个有价值的计算量。

如果你的代码正确地使用了惰性计算并使用了loop/recur,请发布它--我会很感兴趣看看你是如何做到的。


感谢您的深入见解和生动的例子,让人一目了然。在您的例子中,被 cons 的对象(lst)相当小。另一方面,如果被 cons 的对象是通过重要计算得出的结果,那么似乎仍然有意义像这样调用 lazy-seq(抱歉它没有格式化):`(loop [.. lst nil] .. (if termination-condn-met? lst (recur ... (lazy-seq (cons x (reduce #(process-it % %2) my-large-map))))))`您同意吗? - Don
是的 - 在这种情况下,您正在将lazy-seq包装在一个重要的计算中,因此它非常有用。重要的不是lazy-seq被包装在递归调用周围,而是它被包装在一个非平凡的计算中。递归调用只是一种常见的非平凡计算。 - Retief

0
从你给出的例子来看,为什么要返回 map?调用者可以只使用 (into {} s) 从 seq 构建 map。

抱歉,我的表述不够清晰。调用者无法构建地图,因为定义地图的MapEntry对可能与序列中的对不同。在我上面的示例中,它们只是以类似的方式相关联。我的意思是:d和a可以在一个MapEntry中一起,而这个MapEntry不是序列中的一对。此外,对中2个值的顺序可能不同。我会修复我的示例以澄清这一点。 - Don
您可以有两个惰性序列,由调用者构建成映射。 - Retief

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接