【问题标题】:mapcat breaking the lazyness打破懒惰的mapcat
【发布时间】:2014-03-23 12:10:51
【问题描述】:

我有一个产生惰性序列的函数,称为 a-function。

如果我运行代码:

(map a-function a-sequence-of-values) 

它按预期返回一个惰性序列。

但是当我运行代码时:

(mapcat a-function a-sequence-of-values) 

它打破了我函数的懒惰。事实上,它把代码变成了

(apply concat (map a-function a-sequence-of-values)) 

所以它需要在连接这些值之前实现映射中的所有值。

我需要的是一个函数,它可以根据需要连接地图函数的结果,而无需事先实现所有地图。

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

(defn my-mapcat
  [f coll]
  (lazy-seq
   (if (not-empty coll)
     (concat
      (f (first coll))
      (my-mapcat f (rest coll))))))

但我不敢相信 clojure 还没有完成一些事情。你知道clojure是否有这样的功能吗?只有少数人和我有同样的问题?

我还发现了一个处理相同问题的博客:http://clojurian.blogspot.com.br/2012/11/beware-of-mapcat.html

【问题讨论】:

    标签: clojure concat lazy-sequences


    【解决方案1】:

    惰性序列生产和消费不同于惰性求值。

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

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

    您的函数my-mapcat 通过将参数包装在 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]
    

    所以,seq-of-seqs的4个元素都实现了,但是它的组成序列都没有实现,连接的序列也没有实现。

    为什么是 4?因为concat 的适用arity 重载版本需要4 个参数[x y & 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)
    

    很遗憾,您不能apply 宏。

    延迟评估的另一种解决方案是使用 thunk 进行包装,这正是您所做的。

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 注意apply 是惰性的也很重要,等待它的第一个参数的惰性。
      • 我不会说apply 是懒惰的。而是严格。但当然它不会改变所应用函数的语义。换句话说,当apply 本身被调用时,apply 肯定会调用它的第一个参数。
      • 并且 map 总是完全评估它的第一个参数,我认为“如果它的第一个参数是惰性的,那么惰性”是准确的
      • 在这个问题的上下文中,apply 是懒惰的。没有暗示(apply concat args) 必须完全实现序列args 才能将其应用于concat。为了说明,这将返回 nil: (apply (fn [& args]) (range))
      • “(更多内容见下文)”:我认为这是指 A. Webb 的答案,目前它在您的答案之上。
      猜你喜欢
      • 2014-03-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-06
      • 1970-01-01
      • 2011-07-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多