【问题标题】:How does foldr work?文件夹如何工作?
【发布时间】:2010-12-17 23:34:42
【问题描述】:

谁能解释foldr 是如何工作的?

举个例子:

Prelude> foldr (-) 54 [10, 11]
53
Prelude> foldr (\x y -> (x+y)/2) 54 [12, 4, 10, 6]
12.0

我对这些处决感到困惑。有什么建议吗?

【问题讨论】:

    标签: haskell combinators fold


    【解决方案1】:

    理解 foldr 的最简单方法是重写你要折叠的列表,而不加糖。

    [1,2,3,4,5] => 1:(2:(3:(4:(5:[]))))
    

    现在foldr f x 所做的是将每个: 替换为中缀形式的f 并将[] 替换为x 并评估结果。

    例如:

    sum [1,2,3] = foldr (+) 0 [1,2,3]
    
    [1,2,3] === 1:(2:(3:[]))
    

    所以

    sum [1,2,3] === 1+(2+(3+0)) = 6
    

    【讨论】:

    • 确实是最好的解释。与 Erik Meijer 的描述相同,即 foldr 只不过是基本情况的替代品,即 []cons 运算符,带有您选择的累加器和函数。
    • 顺便说一句,这完美地展示了列表如何在 Church 的 lambda 演算中使用 - 基本上列表是未应用的foldr,因此用户可以决定如何处理“cons”和“零”案例
    【解决方案2】:

    foldr 从列表的右端开始,并使用您提供的函数将每个列表条目与累加器值组合在一起。结果是累加器在所有列表元素中“折叠”后的最终值。它的类型是:

    foldr :: (a -> b -> b) -> b -> [a] -> b
    

    从中可以看出,列表元素(a 类型)是给定函数的第一个参数,累加器(b 类型)是第二个参数。

    对于你的第一个例子:

    Starting accumulator = 54
    11 -   54  = -43
    10 - (-43) =  53
    
            ^  Result from the previous line
    
     ^ Next list item
    

    所以你得到的答案是 53。

    第二个例子:

    Starting accumulator = 54
    (6  + 54) / 2 = 30
    (10 + 30) / 2 = 20
    (4  + 20) / 2 = 12
    (12 + 12) / 2 = 12
    

    所以结果是 12。

    编辑:我的意思是添加,这是针对有限列表的。 foldr 也可以在无限列表上工作,但我认为最好先了解有限情况。

    【讨论】:

    • 你确定 foldr 可以在无限列表上工作吗?据我了解,括号意味着它必须首先评估最后一个元素。
    • 你可以实现'map',例如,使用foldr,它甚至可以在无限列表上工作。之所以有效,是因为 (:) 在其第二个参数或英文中是非严格的,因为结果列表的尾部在您沿​​着它走时可能保持未评估。周围有很多网页比我能更好地解释这一点,但正如我所说,你需要付出一些努力才能理解它。协调 foldr 的行为方式 和它的 定义 方式并非易事。
    • 这是完全错误的。 foldr 不是“从右边开始”。它是右关联的。您可以通过阅读Foldable[] 实例的源代码来验证hackage.haskell.org/package/base-4.10.0.0/docs/src/…
    • 这对我这个 Haskell 初学者来说是最容易理解的答案。我还不需要了解foldr 背后的所有理论和实现细节。我只需要简单解释一下foldr 是如何得到结果的。如果解释过于简化也没关系。 Haskell 与我学过的任何命令式或函数式语言都非常不同。我只需要充分了解foldr 就可以继续解决Haskell 99 问题。正如我所做的那样,我的头脑将更完整地了解 Haskell 的全部内容。感谢您让事情尽可能简单。
    • @devdanke 并不过分简化,这是错误的。根据这个答案foldr (||) False [True, error "ERROR"] 必须导致错误,但它返回True。不,foldr 被定义为 foldr (||) z [] = zfoldr (||) z (x:xs) = x || foldr (||) z xs 就是这样。
    【解决方案3】:

    这有助于理解foldrfoldl 之间的区别。为什么foldr被称为“对折”?

    最初我以为是因为它从右到左消耗元素。然而foldrfoldl 都是从左到右使用列表的。

    • foldl 评估从左到右(左关联)
    • foldr 评估从右到左(右关联)

    我们可以通过一个使用关联性很重要的运算符的示例来明确这种区别。我们可以使用一个人类示例,例如操作符“eats”:

    foodChain = (human : (shark : (fish : (algae : []))))
    
    foldl step [] foodChain
      where step eater food = eater `eats` food  -- note that "eater" is the accumulator and "food" is the element
    
    foldl `eats` [] (human : (shark : (fish : (algae : []))))
      == foldl eats (human `eats` shark)                              (fish : (algae : []))
      == foldl eats ((human `eats` shark) `eats` fish)                (algae : [])
      == foldl eats (((human `eats` shark) `eats` fish) `eats` algae) []
      ==            (((human `eats` shark) `eats` fish) `eats` algae)
    

    这个foldl的语义是:一个人吃了一些鲨鱼,然后同一个吃过鲨鱼的人又吃了一些鱼,等等。食者是累加器。

    对比:

    foldr step [] foodChain
        where step food eater = eater `eats` food.   -- note that "eater" is the element and "food" is the accumulator
    
    foldr `eats` [] (human : (shark : (fish : (algae : []))))
      == foldr eats (human `eats` shark)                              (fish : (algae : []))))
      == foldr eats (human `eats` (shark `eats` (fish))               (algae : [])
      == foldr eats (human `eats` (shark `eats` (fish `eats` algae))) []
      ==            (human `eats` (shark `eats` (fish `eats` algae) 
    

    这个foldr的语义是:一个人吃了一条已经吃过鱼的鲨鱼,而鱼已经吃了一些藻类。食物是蓄能器。

    foldlfoldr 从左到右“剥离”食客,所以这不是我们将 foldl 称为“左折叠”的原因。相反,评估的顺序很重要。

    【讨论】:

      【解决方案4】:

      想想foldr 很像definition

       -- if the list is empty, the result is the initial value z
       foldr f z []     = z                  
       -- if not, apply f to the first element and the result of folding the rest 
       foldr f z (x:xs) = f x (foldr f z xs)
      

      所以例如foldr (-) 54 [10,11]必须等于(-) 10 (foldr (-) 54 [11]),即再次扩展,等于(-) 10 ((-) 11 54)。所以内部运算为11 - 54,即-43;并且外部操作是10 - (-43),即10 + 43,因此您观察到53。对您的第二种情况执行类似的步骤,您将再次看到结果如何形成!

      【讨论】:

      • source code中,定义为foldr f z t = appEndo (foldMap (Endo #. f) t) z
      • @Student 它改变了。过去就像在这个答案中一样,然后他们改变了它。但结果是一样的:这个答案中的方程式遵循今天的定义。
      【解决方案5】:

      foldr 表示从右边折叠,所以foldr (-) 0 [1, 2, 3] 产生(1 - (2 - (3 - 0)))。相比之下,foldl 产生 (((0 - 1) - 2) - 3)

      当操作符不是commutativefoldlfoldr会得到不同的结果。

      在您的情况下,第一个示例扩展为 (10 - (11 - 54)),得到 53。

      【讨论】:

        【解决方案6】:

        理解foldr 的一个简单方法是:它将每个列表构造函数替换为所提供函数的应用程序。您的第一个示例将转换为:

        10 - (11 - 54)

        来自:

        10 : (11 : [])

        我从 Haskell Wikibook 中得到的一条很好的建议可能在这里有用:

        通常,您应该在可能是无限的列表或折叠正在构建数据结构的列表上使用foldr,如果已知列表是有限的并且归结为单个值,则应使用foldl'foldl(没有打勾)应该很少使用。

        【讨论】:

          【解决方案7】:

          我一直认为http://foldr.com 是一个有趣的插图。请参阅Lambda the Ultimate 帖子。

          【讨论】:

          • 那些链接似乎已失效。知道发生了什么吗?
          • 当他发布链接时,它已经死了,IIRC。
          • 1+(1+(1+(1+(1+(1+(1+(1+(1+(…)))))))))=∞ 等。链接对我有用,但数学笑话不行。
          • @WillNess 这就像 0.99999... = 1 一样有趣。
          【解决方案8】:

          仔细阅读和比较此处提供的其他答案应该已经清楚了这一点,但值得注意的是,接受的答案可能对初学者有点误导。正如其他评论者所指出的,在 Haskell 中执行的计算文件夹不是“从列表的右手端开始”;否则,foldr 永远无法在无限列表上工作(它在 Haskell 中,在正确的条件下)。

          Haskell 的 foldr 函数的 source code 应该清楚地说明这一点:

          foldr k z = go
                    where
                      go []     = z
                      go (y:ys) = y `k` go ys
          

          每个递归计算都将最左边的原子列表项与列表尾部的递归计算结合起来,即:

          a\[1\] `f` (a[2] `f` (a[3] `f` ... (a[n-1] `f` a[n])  ...))
          

          其中a[n] 是初始累加器。

          因为缩减是“在 Haskell 中懒惰地”完成的,实际上是从左侧开始。这就是我们所说的“惰性求值”,它是 Haskell 著名的一个显着特征。并且理解Haskell的foldr的操作很重要;因为,事实上,foldr 构建并减少计算从左侧递归可以短路的二元运算符有机会,允许无限列表在适当的情况下减少foldr

          如果说foldrfoldl 中的r ("right") 和l ("left") 指的是右关联 和 left associativity,要么就这样,要么尝试解释 Haskell 的惰性求值机制的含义。

          为了完成您的示例,按照foldr 源代码,我们构建了以下表达式:

          Prelude> foldr (-) 54 [10, 11]
          
          ->
          
          10 - [11 - 54] = 53
          

          再说一遍:

          foldr (\x y -> (x + y) / 2) 54 [12, 4, 10, 6]
          
          ->
          
          (12 + (4 + (10 + (6 + 54) / 2) / 2) / 2) / 2 = 12
          

          【讨论】:

          • 非常感谢!,您的回答让我真正了解了foldr 和无限列表中到底发生了什么。
          【解决方案9】:

          我认为以简单的方式实现 map、foldl 和 foldr 有助于解释它们的工作原理。实例也有助于我们理解。

            myMap f [] = []
            myMap f (x:xs) = f x : myMap f xs
          
            myFoldL f i [] = i
            myFoldL f i (x:xs) = myFoldL f (f i x) xs
          
            > tail [1,2,3,4] ==> [2,3,4]
            > last [1,2,3,4] ==> 4
            > head [1,2,3,4] ==> 1
            > init [1,2,3,4] ==> [1,2,3]
          
            -- where f is a function,
            --  acc is an accumulator which is given initially
            --  l is a list.
            --
            myFoldR' f acc [] = acc
            myFoldR' f acc l = myFoldR' f (f acc (last l)) (init l)
          
            myFoldR f z []     = z
            myFoldR f z (x:xs) = f x (myFoldR f z xs)
          
            > map (\x -> x/2) [12,4,10,6] ==> [6.0,2.0,5.0,3.0]
            > myMap (\x -> x/2) [12,4,10,6] ==> [6.0,2.0,5.0,3.0]
          
            > foldl (\x y -> (x+y)/2) 54 [12, 4, 10, 6] ==> 10.125
            > myFoldL (\x y -> (x+y)/2) 54 [12, 4, 10, 6] ==> 10.125
          
              foldl from above: Starting accumulator = 54
                (12  + 54) / 2 = 33
                (4 + 33) / 2 = 18.5
                (10  + 18.5) / 2 = 14.25
                (6 + 14.25) / 2 = 10.125`
          
           > foldr (++) "5" ["1", "2", "3", "4"] ==> "12345"
          
           > foldl (++) "5" ["1", "2", "3", "4"] ==> “51234"
          
           > foldr (\x y -> (x+y)/2) 54 [12,4,10,6] ==> 12
           > myFoldR' (\x y -> (x+y)/2) 54 [12,4,10,6] ==> 12
           > myFoldR (\x y -> (x+y)/2) 54 [12,4,10,6] ==> 12
          
              foldr from above: Starting accumulator = 54
                  (6  + 54) / 2 = 30
                  (10 + 30) / 2 = 20
                  (4  + 20) / 2 = 12
                  (12 + 12) / 2 = 12
          

          【讨论】:

            【解决方案10】:

            好的,让我们看看参数:

            • 一个函数(接受一个列表元素和一个与其返回的值相同类型的值(可能的部分结果));
            • 空列表特殊情况的初始结果规范
            • 一个列表;

            返回值:

            • 一些最终结果

            它首先将函数应用于列表中的最后一个元素和空列表结果。然后它用这个结果和前一个元素重新应用函数,依此类推,直到它需要一些当前结果和列表的第一个元素来返回最终结果。

            Fold 使用一个接收元素和一些先前折叠结果的函数围绕初始结果“折叠”一个列表。它对每个元素重复此操作。因此,foldr 从列表的末尾或右侧开始执行此操作。

            folr f emptyresult [1,2,3,4] 变成 f(1, f(2, f(3, f(4, emptyresult) ) ) ) 。现在只需在评估中加上括号即可。

            需要注意的重要一点是,提供的函数 f 必须将其自己的返回值作为其第二个参数处理,这意味着两者必须具有相同的类型。

            来源:my post 如果您认为它可能会有所帮助,我会从命令式非curried javascript 的角度来看待它。

            【讨论】:

              【解决方案11】:

              此 wiki 页面中的图片形象化了foldr(以及foldl)的想法:

              比如foldr (-) 0 [1,2,3]的结果是2,可以形象化为:

                -
               / \
              1   -
                 / \
                2   -
                   / \
                  3   0
              

              即(从下往上):

              1 - ( -1 )      = 2
                  2 - ( 3 )
                      3 - 0
              

              所以foldr (\x y -> (x+y)/2) 54 [12, 4, 10, 6] 正在通过以下方式计算:

              12 `f` (12.0)          = 12.0
                   4 `f` (20.0)
                      10 `f` (30.0)
                           6 `f` 54
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2017-03-07
                • 1970-01-01
                • 1970-01-01
                • 2014-04-09
                • 1970-01-01
                • 1970-01-01
                • 2016-03-29
                • 2021-08-31
                相关资源
                最近更新 更多