使用foldr编写foldl


79

真实世界的Haskell中,关于函数式编程的第4章:

用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个参数,我完全感到困惑!

2
对于真正的受虐狂,step = curry $ uncurry (&) <<< (flip f) *** (.)
Weiwei Zhou

Answers:


99

一些解释是为了命令!

id函数有什么作用?扮演什么角色?为什么在这里需要它?

id恒等函数id x = x以及建立与功能的链时被用作等效的零功能组合物(.)。您可以在Prelude中找到它的定义

在上面的示例中,id函数是lambda函数中的累加器吗?

累加器是通过重复功能应用程序建立的功能。由于我们将累加器命名为,所以没有显式的lambda step。如果需要,可以使用lambda编写它:

foldl f a bs = foldr (\b g x -> g (f x b)) id bs a

或者像格雷厄姆·赫顿Graham Hutton)那样写道

5.1foldl操作员

现在,让我们从suml示例中进行概括,并考虑foldl通过使用f组合值和以值v作为起始值的函数从左至右顺序处理列表元素的标准运算符:

foldl :: (β → α → β) → β → ([α] → β)
foldl f v [ ] = v
foldl f v (x : xs) = foldl f (f v x) xs

使用此运算符,suml可以简单地通过重新定义suml = foldl (+) 0。使用可以通过简单的方式定义许多其他功能foldl。例如,reverse可以使用foldl以下方法重新定义标准函数:

reverse :: [α] → [α]
reverse = foldl (λxs x → x : xs) [ ]

此定义比我们最初使用fold的定义更有效,因为它避免了将低效的append运算符(++)用于列表。

在上一节的功能在计算的简单概括suml显示了如何重新定义函数foldl在以下方面fold

foldl f v xs = fold (λx g → (λa → g (f a x))) id xs v

相反,它是不可能重新定义fold来讲foldl,由于这样的事实, foldl在其列表参数的尾部严格,但fold并非如此。还有关于一些有用的“对偶定理”foldfoldl,也是决定哪些操作是最适合于特定的应用程序(伯德,1998年)的一些准则。

foldr的原型是folder ::(a-> b-> b)-> b-> [a]-> b

哈斯克尔程序员会说foldr(a -> b -> b) -> b -> [a] -> b

第一个参数是需要两个参数的函数,但是myFoldl的实现中的step函数使用了3个参数,我完全困惑

这是令人困惑和神奇的!我们玩弄一个小技巧,并用一个函数替换累加器,然后将其应用到初始值以产生结果。

格雷厄姆赫顿解释的伎俩转foldlfoldr以上文章。我们首先记录以下内容的递归定义foldl

foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v []       = v
foldl f v (x : xs) = foldl f (f v x) xs

然后通过对以下内容的静态参数转换来重构它f

foldl :: (a -> b -> a) -> a -> [b] -> a    
foldl f v xs = g xs v
    where
        g []     v = v
        g (x:xs) v = g xs (f v x)

现在让我们重写g以便v向内浮动:

foldl f v xs = g xs v
    where
        g []     = \v -> v
        g (x:xs) = \v -> g xs (f v x)

这与g将一个参数视为函数一样,它返回一个函数:

foldl f v xs = g xs v
    where
        g []     = id
        g (x:xs) = \v -> g xs (f v x)

现在我们有了g一个递归遍历列表的函数,应用了一些函数f。最终值是恒等函数,每个步骤也将产生一个函数。

但是,我们已经在列表上有了一个非常相似的递归函数foldr

2折叠运算符

fold运营商有其递归论(克莱尼,1952年)的起源,而采用fold作为编程语言日期的核心概念回APL的减少运营商(艾弗森,1962年),后来到FP的插入操作符(巴克斯(1978年)。在Haskell中,fold列表的运算符可以定义如下:

fold :: (α → β → β) → β → ([α] → β)
fold f v [ ] = v
fold f v (x : xs) = f x (fold f v xs)

即,给定的函数f类型的α → β → β和值v类型β,函数 fold f v处理类型的列表[α],得到类型的值β通过更换零构造[]在列表由所述值的末尾v,并且每个缺点的构造(:)通过在列表中功能f。以这种方式,fold运算符封装了用于处理列表的简单递归模式,其中列表的两个构造函数被其他值和函数简单地替换。列表上许多熟悉的函数使用定义了一个简单的符号fold

这看起来与我们的g函数非常类似的递归方案。现在的诀窍是:使用手头上所有可用的魔术(也称为Bird,Meertens和Malcolm),我们应用一条特殊规则,即fold通用属性,它是g处理列表的函数的两个定义之间的等价关系,表示为:

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

当且仅当

g = fold f v

因此,folds的通用属性指出:

    g = foldr k v

g对于k和,其中必须等于两个方程式v

    g []     = v
    g (x:xs) = k x (g xs)

从我们早期的折叠设计中,我们知道v == id。但是对于第二个方程,我们需要计算的定义k

    g (x:xs)         = k x (g xs)        
<=> g (x:xs) v       = k x (g xs) v      -- accumulator of functions
<=> g xs (f v x)     = k x (g xs) v      -- definition of foldl
<=  g' (f v x)       = k x g' v          -- generalize (g xs) to g'
<=> k = \x g' -> (\a -> g' (f v x))      -- expand k. recursion captured in g'

其中,代我们计算的定义k,并v得到与foldl作为一个定义:

foldl :: (a -> b -> a) -> a -> [b] -> a    
foldl f v xs =
    foldr
        (\x g -> (\a -> g (f v x)))
        id
        xs
        v

递归g被folder组合器代替,累加器成为通过f列表中每个元素的链组成的功能相反的顺序构建的函数(因此,我们向左折叠而不是向右折叠)。

这肯定有点先进,因此要深入了解这种转换(即folds通用属性),使转换成为可能,我建议使用Hutton的教程,链接如下。


参考文献


1
请改正错字k = \x g' -> (\a -> g' (f v x)) (\x g -> (\a -> g (f v x)))
Kamel 2015年

10

考虑以下类型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 -> c与相同a -> (b -> c)

因此,是的,的累加器值foldr是type的函数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


6

(快速浏览一下我的答案[1][2][3][4]以确保您了解Haskell的语法,高阶函数,currying,函数组成,$运算符,infix / prefix运算符,部分和lambdas )

折叠的通用性

只是一个特定类型的递归的编纂。并且通用性属性简单地指出,如果您的递归符合某种形式,则可以根据一些正式规则将其转换为折叠。相反,每个折叠都可以转换为这种递归。再一次,可以将某些递归转换为给出完全相同答案的折叠,而有些递归则不能,并且有一个精确的过程可以做到这一点。

基本上,如果您的递归函数的工作列表上的一个看起来像在左边,你可以将它折叠一个正确的,替代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等效。另外2个例子sum (x:xs) = (+) x (sum xs)f = (+)

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递归,就像在上述功能侧。

  2. 根据上面的公式将这5个函数转换为foldr ,即用f和替换v右侧的fold公式中。

通过文件夹折叠

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

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

找到的第一个递归函数甚至在开始累加之前就已完全扩展,这不是我们所需要的。一种方法是创建一个具有accumulator的递归函数,该函数立即在每个步骤上累加数字(了解有关尾递归的知识,以了解有关递归策略的更多信息):

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'可以。

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的fold的普遍性和表现力教程。得到一些笔和纸,尝试弄清楚他写的所有东西,直到您自己获得大部分折痕为止。如果您不了解某些内容,请不要流汗,您可以稍后再返回,但也不要拖延。


我发现这个答案比公认的答案更简单,更清晰。太糟糕了,它的投票数太少了……
千兆字节(

5

这是我foldl可以用表示的证明,foldr除了step函数引入的意大利面之外,我发现这很简单。

命题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将会计算的不是一个值,而是一个咖喱函数,然后将其应用于z。有两种情况需要调查以确定是否myfoldlfoldl

  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
    

由于在2.中,第一行和最后一行在两种情况下都具有相同的形式,因此可以使用它将列表向下折叠直到xs == [],在这种情况下,1.保证结果相同。因此,归纳法myfoldl == foldl


2

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

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

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

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]

是皇家之路还是什么?


2

减票前,请阅读以下段落

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

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

我了解这个简单练习的漫长路要走

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

以下示例调用会发生什么

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

可以通过下图(位于Wikipedia上,但我首先在另一个答案上看到它)来可视化:

          _____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

还应该清楚,一个呼叫s带有参数xy是的(满)应用y到的部分应用程序f的唯一理由x。由于fto的部分应用x可以写为lambda (\z -> f z x),因此完全应用y它就可以生成lambda (\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,但是yins x y = y . (\z -> f z x)表示s 4 ?(的解释,以及所有其他)的解释缺少由最左边的lambda组成的引导函数。

那只是我们的?功能:是时候给它一个存在的理由了,除了在对的调用中占据一席之地foldr。为了不改变生成的功能,我们可以选择它是什么?答:关于组合运算符id标识元素(.)

  s          s 1 (…) == id . (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2) . (\z -> f z 1)
 / \
1   s        s 2 (…) == id . (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2)
   / \
  2   s      s 3 (…) == id . (\z -> f z 4) . (\z -> f z 3)
     / \
    3   s    s 4 id  == id . (\z -> f z 4)
       / \
      4   id

所以寻找的功能是

myFoldl f z xs = foldr (\x g a -> g (f a x)) id xs z

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 = ...

1
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)

好吧,至少这对我有所帮助。即使不是很正确。


实际序列是foldr step id [1, 2, 3] 0- > step 1 (foldr step id [2, 3]) 0- > (foldr step id [2, 3]) (0 + 1)- > step 2 (foldr step id [3]) (0 + 1)- > (foldr step id [3]) ((0 + 1) + 2)- > step 3 (foldr step id []) ((0 + 1) + 2)- > (foldr step id []) (((0 + 1) + 2) + 3)- > id (((0 + 1) + 2) + 3)
尼斯,

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那个foldr step id [x1 .. xn] = foldr rc id [f1 .. fn]。很容易找到step = \x g z -> g (f z x)

在3个步骤中,foldlusing的定义foldr很明确:

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

证明正确性:

foldl f z xs = foldr (\x g z -> g (f z x)) id xs z
             = step x1 (foldr step id [x2 .. xn]) z
             = s1 (foldr step id [x2 .. xn]) z
             = s1 (step x2 (foldr step id [x3 .. xn])) z
             = s1 (s2 (foldr step id [x3 .. xn])) z
             = ..
             = s1 (s2 (.. (sn (foldr step id [])) .. )) z
             = s1 (s2 (.. (sn id) .. )) z
             = (s2 (.. (sn id) .. )) (f z x1)
             = s2 (s3 (.. (sn id) .. )) (f z x1)
             = (s3 (.. (sn id) .. )) (f (f z x1) x2)
             = ..
             = sn id (f (.. (f (f z x1) x2) .. ) xn-1)
             = id (f (.. (f (f z x1) x2) .. ) xn)
             = f (.. (f (f z x1) x2) .. ) xn

in which xs = [x1 .. xn], si = step xi = \g z -> g (f z xi)

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.