使用foldr编写foldl

时间:2011-05-30 03:21:00

标签: haskell recursion fold

Real World Haskell ,第4章。 Functional Programming

用foldr写下foldl:

-- file: ch04/Fold.hs
myFoldl :: (a -> b -> a) -> a -> [b] -> a

myFoldl f z xs = foldr step id xs z
    where step x g a = g (f a x)

上面的代码让我很困惑,有人打电话给 dps 用一个有意义的名字重写它,以使它更清晰:

myFoldl stepL zeroL xs = (foldr stepR id xs) zeroL
where stepR lastL accR accInitL = accR (stepL accInitL lastL)

其他人,Jef G,然后通过提供一个例子并逐步展示基础机制做了出色的工作:

myFoldl (+) 0 [1, 2, 3]
= (foldR step id [1, 2, 3]) 0
= (step 1 (step 2 (step 3 id))) 0
= (step 1 (step 2 (\a3 -> id ((+) a3 3)))) 0
= (step 1 (\a2 -> (\a3 -> id ((+) a3 3)) ((+) a2 2))) 0
= (\a1 -> (\a2 -> (\a3 -> id ((+) a3 3)) ((+) a2 2)) ((+) a1 1)) 0
= (\a1 -> (\a2 -> (\a3 -> (+) a3 3) ((+) a2 2)) ((+) a1 1)) 0
= (\a1 -> (\a2 -> (+) ((+) a2 2) 3) ((+) a1 1)) 0
= (\a1 -> (+) ((+) ((+) a1 1) 2) 3) 0
= (+) ((+) ((+) 0 1) 2) 3
= ((0 + 1) + 2) + 3

但我还是不能完全理解,这是我的问题:

  1. id功能是什么?有什么作用?我们为什么要在这里需要它?
  2. 在上面的示例中,id函数是lambda函数中的累加器吗?
  3. foldr的原型是foldr :: (a -> b -> b) -> b -> [a] -> b,第一个参数是一个需要两个参数的函数,但myFoldl实现中的step函数使用3个参数,我完全混淆了! < / LI>

    有没有人可以帮助我?非常感谢!

9 个答案:

答案 0 :(得分:91)

答案 1 :(得分:9)

考虑foldr的类型:

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

step的类型类似于b -> (a -> a) -> a -> a。由于步骤已传递到foldr,我们可以得出结论,在这种情况下,折叠的类型为(b -> (a -> a) -> (a -> a)) -> (a -> a) -> [b] -> (a -> a)

不要对a在不同签名中的不同含义感到困惑;它只是一个类型变量。另外,请记住功能箭头是右关联的,因此a -> b -> ca -> (b -> c)相同。

所以,是的,foldr的累加器值是a -> a类型的函数,初始值为id。这是有道理的,因为id是一个不做任何事情的函数 - 这与在列表中添加所有值时以零作为初始值开始的原因相同。

对于带有三个参数的step,请尝试重写它:

step :: b -> (a -> a) -> (a -> a)
step x g = \a -> g (f a x)

这会让您更容易看到发生了什么吗?它需要一个额外的参数,因为它返回一个函数,并且它的两种写入方式是等价的。另请注意foldr之后的额外参数:(foldr step id xs) z。括号中的部分是折叠本身,它返回一个函数,然后应用于z

答案 2 :(得分:5)

(快速略读我的答案[1][2][3][4],以确保您了解Haskell的语法,更高阶函数,currying,函数组合,$运算符,中缀/前缀运算符,节和lambdas)

折叠的普遍性

fold只是某种递归的编纂。普遍性属性简单地说明,如果你的递归符合某种形式,它可以根据一些正式规则转换成折叠。相反,每个折叠都可以转换成这种递归。再一次,一些递归可以转换成折叠,给出完全相同的答案,并且一些递归不能,并且有一个确切的过程来做到这一点。

基本上,如果您的递归函数在列表上工作,在左侧上,您可以将其转换为折叠一个,替换fv实际上是什么。

g []     = v              ⇒
g (x:xs) = f x (g xs)     ⇒     g = foldr f v

例如:

sum []     = 0   {- recursion becomes fold -}
sum (x:xs) = x + sum xs   ⇒     sum = foldr 0 (+)

此处v = 0sum (x:xs) = x + sum xs相当于sum (x:xs) = (+) x (sum xs),因此f = (+)。还有2个例子

product []     = 1
product (x:xs) = x * product xs  ⇒  product = foldr 1 (*)

length []     = 0
length (x:xs) = 1 + length xs    ⇒  length = foldr (\_ a -> 1 + a) 0
  

<强>练习:

     
      
  1. 递归执行mapfilterreverseconcatconcatMap,就像上面左边的函数一样/ em> side。

  2.   
  3. 根据上面的公式将这5个函数转换为foldr ,即在右侧的折叠公式中替换fv

  4.   

通过foldr折叠

如何编写一个从左到右对数字求和的递归函数?

sum [] = 0     -- given `sum [1,2,3]` expands into `(1 + (2 + 3))`
sum (x:xs) = x + sum xs

第一个递归函数在开始累加之前完全展开,这不是我们需要的。一种方法是创建一个具有 accumulator 的递归函数,它会立即在每一步上添加数字(阅读tail recursion以了解有关递归策略的更多信息):

suml :: [a] -> a
suml xs = suml' xs 0
  where suml' [] n = n   -- auxiliary function
        suml' (x:xs) n = suml' xs (n+x)

好的,停下来!在GHCi中运行此代码并确保您了解它的工作原理,然后仔细并仔细地继续。 suml无法通过折叠重新定义,但suml'可以。{/ p>

suml' []       = v    -- equivalent: v n = n
suml' (x:xs) n = f x (suml' xs) n
函数定义中的

suml' [] n = n,对吧?来自通用属性公式的v = suml' []。这给了v n = n一个函数,它立即返回它收到的任何函数:v = id。我们来计算f

suml' (x:xs) n = f x (suml' xs) n
-- expand suml' definition
suml' xs (n+x) = f x (suml' xs) n
-- replace `suml' xs` with `g`
g (n+x)        = f x g n

因此,suml' = foldr (\x g n -> g (n+x)) id,因此,suml = foldr (\x g n -> g (n+x)) id xs 0

foldr (\x g n -> g (n + x)) id [1..10] 0 -- return 55

现在我们只需要概括,用变量函数替换+

foldl f a xs = foldr (\x g n -> g (n `f` x)) id xs a
foldl (-) 10 [1..5] -- returns -5

结论

现在阅读Graham Hutton的A tutorial on the universality and expressiveness of fold。得到一些笔和纸,试着找出他写的所有东西,直到你自己得到大部分的折叠。如果你不理解某些东西,不要出汗,你可以随时回来,但也不要拖延。

答案 3 :(得分:4)

我的证明foldl可以用foldr来表达,除了step函数引入的名称spaghetti之外,我发现它非常简单。

命题是foldl f z xs等同于

myfoldl f z xs = foldr step_f id xs z
        where step_f x g a = g (f a x)

首先要注意的是,第一行的右侧实际上被评估为

(foldr step_f id xs) z

因为foldr只需要三个参数。这已经暗示foldr将计算不是值而是计算curried函数,然后将其应用于z。有两种情况需要调查,以确定myfoldl是否为foldl

  1. 基本情况:空列表

      myfoldl f z []
    = foldr step_f id [] z    (by definition of myfoldl)
    = id z                    (by definition of foldr)
    = z
    
      foldl f z []
    = z                       (by definition of foldl)
    
  2. 非空列表

      myfoldl f z (x:xs)
    = foldr step_f id (x:xs) z          (by definition of myfoldl)
    = step_f x (foldr step_f id xs) z   (-> apply step_f)
    = (foldr step_f id xs) (f z x)      (-> remove parentheses)
    = foldr step_f id xs (f z x)
    = myfoldl f (f z x) xs              (definition of myfoldl)
    
      foldl f z (x:xs)
    = foldl f (f z x) xs
    
  3. 由于在第2行中,第一行和最后一行在两种情况下都具有相同的形式,因此可以使用它将列表向下折叠到xs == [],在这种情况下1.保证相同的结果。所以通过归纳,myfoldl == foldl

答案 4 :(得分:1)

没有通往数学的皇家之路,甚至也没有Haskell。让

h z = (foldr step id xs) z where   
     step x g =  \a -> g (f a x)

到底是什么h z?假设xs = [x0, x1, x2]
应用foldr的定义:

h z = (step x0 (step x1 (step x2 id))) z 

应用步骤的定义:

= (\a0 -> (\a1 -> (\a2 -> id (f a2 x2)) (f a1 x1)) (f a0 x0)) z

替换为lambda函数:

= (\a1 -> (\a2 -> id (f a2 x2)) (f a1 x1)) (f z x0)

= (\a2 -> id (f a2 x2)) (f (f z x0) x1)

= id (f (f (f z x0) x1) x2)

应用id:

的定义
= f (f (f z x0) x1) x2

应用foldl的定义:

= foldl f z [x0, x1, x2]

是皇家之路还是什么?

答案 5 :(得分:1)

这可能会有所帮助,我尝试以不同的方式进行扩展。

myFoldl (+) 0 [1,2,3] = 
foldr step id [1,2,3] 0 = 
foldr step (\a -> id (a+3)) [1,2] 0 = 
foldr step (\b -> (\a -> id (a+3)) (b+2)) [1] 0 = 
foldr step (\b -> id ((b+2)+3)) [1] 0 = 
foldr step (\c -> (\b -> id ((b+2)+3)) (c+1)) [] 0 = 
foldr step (\c -> id (((c+1)+2)+3)) [] 0 = 
(\c -> id (((c+1)+2)+3)) 0 = ...

答案 6 :(得分:1)

拒绝投票之前,请阅读以下段落

我正在为可能会发现这种方法更适合其思维方式的人们发布答案。答案可能包含多余的信息和想法,但这是我解决问题所需要的。此外,由于这是对同一问题的又一个答案,很明显,它与其他答案有很多重叠,但是,它讲述了我如何理解这个概念的故事。

的确,在尝试理解此主题时,我开始写下此笔记作为个人想法的记录。如果我真的掌握了它,我花了整整一天的时间来摸索它的核心。

我很了解这种简单的练习

简单的部分:我们需要确定什么?

以下示例调用会发生什么情况

foldl f z [1,2,3,4]

可以通过以下图表(位于Wikipedia上,但我首先在another answer上看到它)来显示:

          _____results in a number
         /
        f          f (f (f (f z 1) 2) 3) 4
       / \
      f   4        f (f (f z 1) 2) 3
     / \
    f   3          f (f z 1) 2
   / \
  f   2            f z 1
 / \
z   1

(作为一个补充说明,当使用foldl时,不会执行f的每个应用程序,并且这些表达式将按照我上面编写它们的方式进行处理;原则上,可以按照您的计算方式来计算它们走到底,这正是foldl'所做的。)

该练习实质上挑战了我们通过适当地更改步进函数(因此我们使用foldr代替foldl来使用s而不是f)和初始累加器(因此我们使用?而不是z);列表保持不变,否则我们在说什么?

foldr的调用必须如下所示:

foldr s ? [1,2,3,4]

和相应的图是这样的:

    _____what does the last call return?
   /
  s
 / \
1   s
   / \
  2   s
     / \
    3   s
       / \
      4   ? <--- what is the initial accumulator?

通话结果

s 1 (s 2 (s 3 (s 4 ?)))

s?是什么?它们的类型是什么?看起来s是一个两个参数的函数,很像f,但让我们不要下结论。另外,让我们将?搁置一会儿,然后观察一下z一开始起作用就必须发挥1的作用;但是,z如何在可能包含两个参数的s函数的调用中,即在调用s 1 (…)中发挥作用?我们可以通过选择一个s来解决谜题的这一部分,该参数需要3个参数,而不是前面提到的2个参数,因此最外面的调用s 1 (…)将导致一个函数接受一个参数,可以将z传递给!

这意味着我们想要原始呼叫,该呼叫将扩展为

f (f (f (f z 1) 2) 3) 4

等同于

s 1 (s 2 (s 3 (s 4 ?))) z

或者换句话说,我们想要部分应用的功能

s 1 (s 2 (s 3 (s 4 ?)))

等效于以下lambda函数

(\z -> f (f (f (f z 1) 2) 3) 4)

同样,我们所需的“唯一”部分是s?

转折点:识别功能组成

让我们重画前面的图,并在右边写下我们希望对s的每次调用等效于:

  s          s 1 (…) == (\z -> f (f (f (f z 1) 2) 3) 4)
 / \
1   s        s 2 (…) == (\z -> f (f (f    z    2) 3) 4)
   / \
  2   s      s 3 (…) == (\z -> f (f       z       3) 4)
     / \
    3   s    s 4  ?  == (\z -> f          z          4)
       / \
      4   ? <--- what is the initial accumulator?

我希望从该图的结构中可以清楚地看出,每行上的(…)是其下一行的右侧;更好的是,它是从上一个(以下)调用s返回的函数。

还应该清楚的是,使用参数sx的{​​{1}}的调用是y的(完整)应用程序对{{1 }}传递给唯一参数y。由于fx的部分应用可以写为lambda f,因此对其完全应用x会产生lambda (\z -> f z x),在此情况下情况下,我将改写为y;将单词翻译成(\z -> y (f z x))的表达式

y . (\z -> f z x)

(这与s相同,如果重命名变量,则与本书相同。)

最后一位是:累加器的初始“值” s x y = y . (\z -> f z x) 是什么?可以通过扩展嵌套调用以使其组成链来重写上图:

s x y z = y (f z x)

我们在这里看到?只是“堆积”了 s s 1 (…) == (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2) . (\z -> f z 1) / \ 1 s s 2 (…) == (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2) / \ 2 s s 3 (…) == (\z -> f z 4) . (\z -> f z 3) / \ 3 s s 4 ? == (\z -> f z 4) / \ 4 ? <--- what is the initial accumulator? 的连续部分应用,但是s中的f暗示了y的解释(以及其他所有功能)错过了由最左侧的lambda组成的前导功能。

这只是我们的s x y = y . (\z -> f z x)函数:是时候给它一个存在的原因了,除了在对s 4 ?的调用中占据一席之地。为了不改变生成的功能,我们可以选择什么呢?答案:?,关于合成运算符foldr的{​​{3}}。

id

所以寻找的功能是

(.)

答案 7 :(得分:0)

foldr step zero (x:xs) = step x (foldr step zero xs)
foldr _ zero []        = zero

myFold f z xs = foldr step id xs z
  where step x g a = g (f a x)

myFold (+) 0 [1, 2, 3] =
  foldr step id [1, 2, 3] 0
  -- Expanding foldr function
  step 1 (foldr step id [2, 3]) 0
  step 1 (step 2 (foldr step id [3])) 0
  step 1 (step 2 (step 3 (foldr step id []))) 0
  -- Expanding step function if it is possible
  step 1 (step 2 (step 3 id)) 0
  step 2 (step 3 id) (0 + 1)
  step 3 id ((0 + 1) + 2)
  id (((0 + 1) + 2) + 3)

至少,这对我有所帮助。甚至还不太对。

答案 8 :(得分:0)

这个答案使下面的定义很容易通过三个步骤来理解。

-- file: ch04/Fold.hs
myFoldl :: (a -> b -> a) -> a -> [b] -> a

myFoldl f z xs = foldr step id xs z
    where step x g a = g (f a x)

第1步:将功能评估的折衷方式转换为功能组合

foldl f z [x1 .. xn] = z & f1 & .. & fn = fn . .. . f1 z。其中fi = \z -> f z xi

(通过使用z & f1 & f2 & .. & fn表示fn ( .. (f2 (f1 z)) .. )。)

第2步,以foldr的方式表达功能组合

foldr (.) id [f1 .. fn] = (.) f1 (foldr (.) id [f2 .. fn]) = f1 . (foldr (.) id [f2 .. fn])。展开其余部分以得到foldr (.) id [f1 .. fn] = f1 . .. . fn

注意到序列是反向的,我们应该使用(.)的反向形式。定义rc f1 f2 = (.) f2 f1 = f2 . f1,然后定义foldr rc id [f1 .. fn] = rc f1 (foldr (.) id [f2 .. fn]) = (foldr (.) id [f2 .. fn]) . f1。展开其余部分以得到foldr rc id [f1 .. fn] = fn . .. . f1

第3步。将函数列表上的折叠转换为操作数列表上的折叠

找到构成step的{​​{1}}。很容易找到foldr step id [x1 .. xn] = foldr rc id [f1 .. fn]

在3个步骤中,使用step = \x g z -> g (f z x)foldl的定义很明确:

foldr

证明正确性:

  foldl f z xs
= fn . .. . f1 z
= foldr rc id fs z
= foldr step id xs z

如果您发现任何不清楚的地方,请添加评论。 :)