【问题标题】:How do we do both left and right folds in Clojure?我们如何在 Clojure 中同时进行左右折叠?
【发布时间】:2013-05-23 21:48:28
【问题描述】:

Reduce 可以正常工作,但它更像是向左折叠。 还有其他形式的 reduce 可以让我向右折叠吗?

【问题讨论】:

    标签: clojure


    【解决方案1】:

    clojure 标准库只有 fold-left(reduce)的原因其实很微妙,这是因为 clojure 不够懒惰,无法获得 fold-right 的主要好处。

    在像 haskell 这样的语言中,fold-right 的主要好处是它实际上可以短路。 如果我们这样做foldr (&&) True [False, True, True, True, True],它实际被评估的方式是非常有启发性的。它唯一需要评估的是带有 1 个参数的函数 and(第一个 False)。一旦它到达那里,它就知道答案并且不需要评估任何Trues。

    如果你仔细看图片:

    您会看到,虽然从概念上讲,右折叠开始和列表的末尾并移到前面,但实际上,它从列表的前面开始计算。

    这是一个例子,说明惰性/柯里化函数和尾递归可以带来 clojure 无法提供的好处。

    奖金部分(感兴趣的人)

    根据 vemv 的建议,我想提一下,Clojure 向核心命名空间添加了一个新函数,以解决 Clojure 不能具有惰性右折叠的限制。在核心命名空间中有一个名为 reduced 的函数,它允许您使 Clojure 的 reduce 更懒惰。通过告诉它不要查看列表的其余部分,它可以用来短路reduce。例如,如果您想将数字列表相乘,但有理由怀疑该列表偶尔会包含零,并且希望通过在遇到零时不查看列表的其余部分来处理这种情况,您可以编写以下multiply-all 函数(注意使用reduced 表示最终答案是0,不管列表的其余部分是什么)。

    (defn multiply-all [coll]
      (reduce
       (fn [accumulator next-value]
         (if (zero? next-value)
           (reduced 0)
           (* accumulator next-value)))
       1
       coll))
    

    然后为了证明它短路,您可以乘以一个恰好包含零的无限数字列表,并看到它确实以0 的答案终止

    (multiply-all
     (cycle [1 2 3 4 0]))
    

    【讨论】:

    • 非常有趣的问答(+1)。
    • 尾递归在这里真的相关吗?我不认为foldr 是尾递归的;如果是这样,那是某种奇怪的与 thunk 相关的尾递归,我还不明白,很想了解。
    • 在您的回答中是否值得一提新的clojure.core/reduced 函数?
    • vemv,在您给出的示例中,实际上是短路的and导致42不被打印,而不是短路的reduce。如果您尝试其他一些示例,您会看到差异。例如(reduce #(and %1 %2) (cons false (repeat true))) 不会终止,但是惰性语言中的等价右折叠将能够避免尝试评估整个序列。
    • 懒惰与短路有关。在 Clojure 中,序列和宏可以是惰性的,但函数应用程序不是。在 Haskell 中,函数应用程序本身是惰性的(这与它们被柯里化的事实有关)。在 Clojure 中,不可能定义一个函数 multiply,当 0 是左侧参数时,它不会评估右侧参数,因为在 Clojure 中,参数在传递给函数之前会被评估。这就是为什么ifandorif-not 等不是函数的原因。在 Haskell 中,所有这些东西都可以是常规函数。
    【解决方案2】:

    让我们看一下每个可能的定义:

    (defn foldl [f val coll]
      (if (empty? coll) val
        (foldl f (f val (first coll)) (rest coll))))
    
    (defn foldr [f val coll]
      (if (empty? coll) val
        (f (foldr f val (rest coll)) (first coll))))
    

    注意只有foldl在尾部位置,递归调用可以用recur代替。所以recurfoldl 不会占用堆栈空间,而foldr 会。这就是为什么reduce 就像foldl。现在让我们尝试一下:

    (foldl + 0 [1 2 3]) ;6
    (foldl - 0 [1 2 3]) ;-6
    (foldl conj  [] [1 2 3]) ;[1 2 3]
    (foldl conj '() [1 2 3]) ;(3 2 1)
    
    (foldr + 0 [1 2 3]) ;6
    (foldr - 0 [1 2 3]) ;-6
    (foldr conj  [] [1 2 3]) ;[3 2 1]
    (foldr conj '() [1 2 3]) ;(1 2 3)
    

    你有什么理由想要弃牌吗?我认为foldr 最常见的用法是将列表从前到后放在一起。在 Clojure 中我们不需要它,因为我们可以只使用向量来代替。避免堆栈溢出的另一个选择是使用惰性序列:

    (defn make-list [coll]
      (lazy-seq
        (cons (first coll) (rest coll))))
    

    所以,如果你想正确折叠,一些有效的选择是

    1. 改用矢量。
    2. 使用惰性序列。
    3. 使用reduced短路reduce
    4. 如果您真的想潜入兔子洞,请使用换能器。

    【讨论】:

      猜你喜欢
      • 2015-04-12
      • 2014-09-03
      • 2017-04-11
      • 1970-01-01
      • 2020-05-04
      • 2020-01-21
      • 2011-11-15
      • 2013-06-28
      • 2013-11-13
      相关资源
      最近更新 更多