81

真实世界的 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 个参数,我完全糊涂了!
4

9 回答 9

106

一些解释是有序的!

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

或者正如格雷厄姆赫顿所写的那样:

5.1foldl运营商

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

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

相反,不可能根据 重新定义foldfoldl因为 foldl它的列表参数的尾部是严格的,但fold不是。有许多有用的关于fold和的“对偶定理” foldl,还有一些用于决定哪个算子最适合特定应用的指南(Bird,1998 年)。

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

Haskell 程序员会说的类型是。foldr(a -> b -> b) -> b -> [a] -> b

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

这是令人困惑和神奇的!我们玩了一个把戏,用一个函数替换累加器,然后将其应用于初始值以产生结果。

Graham Hutton在上面的文章中解释了foldl变成的技巧。foldr我们首先写下 的递归定义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)

现在让我们重写gv向内浮动:

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运算符起源于递归理论 (Kleene, 1952),而作为编程语言中的中心概念的使用可以fold追溯到 APL 的归约运算符 (Iverson, 1962),然后是 FP 的插入运算符 (Backus , 1978)。在 Haskell 中,fold列表的操作符可以定义如下:

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

也就是说,给定一个 type 的函数f和一个 type的α → β → β值,该函数 处理一个 type 列表,通过将列表末尾的nil 构造函数替换为value ,并将列表中的每个 cons 构造函数替换为功能。以这种方式,操作符封装了一个简单的递归模式来处理列表,其中列表的两个构造函数被其他值和函数简单地替换。列表上许多熟悉的函数都有一个简单的定义,使用.vβfold f v[α]β[]v(:)ffoldfold

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

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

当且仅当

g = fold f v

因此,折叠的普遍属性表明:

    g = foldr k v

其中g必须等价于两个方程,对于一些kv

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

从我们早期的 foldl 设计中,我们知道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被 foldr 组合器替换,并且累加器变成了一个函数,该函数通过f列表中每个元素的组合链以相反的顺序构建(因此我们向左折叠而不是向右折叠)。

这肯定有点高级,所以要深入理解这种转换,folds 的通用属性,使转换成为可能,我推荐 Hutton 的教程,链接如下。


参考

于 2011-05-30T04:23:52.133 回答
11

考虑以下类型foldr

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

而类型step类似于b -> (a -> a) -> a -> a. 由于 step 被传递到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.

于 2011-05-30T03:56:04.753 回答
6

(快速浏览我的答案[1][2][3][4]以确保您了解 Haskell 的语法、高阶函数、柯里化、函数组合、$ 运算符、中缀/前缀运算符、节和 lambdas )

折叠的普遍性质

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

基本上,如果您的递归函数适用于左侧的列表,您可以将其转换为向右折叠一个f,替换v为实际存在的内容。

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 = 0andsum (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. 实现map, filter, reverse,concatconcatMap递归,就像上面左边的函数一样。

  2. 根据上面的一个公式将这5个函数转换成foldr ,也就是把和代f右边v的fold公式中。

通过 foldr 折叠

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

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

find 的第一个递归函数甚至在开始累加之前就完全扩展了,这不是我们需要的。一种方法是创建一个具有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 的普遍性和表现力的教程。拿一些笔和纸,试着计算他写的所有东西,直到你自己得出大部分的折痕。不明白的事情不要着急,你可以稍后再回来,但也不要拖延。

于 2015-02-26T22:50:27.753 回答
5

foldl这是我可以用 表示的证明,除了函数引入foldr的名称 spaghetti 之外,我发现它非常简单。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将计算的不是一个值,而是一个 curried 函数,然后将其应用于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.

于 2012-10-09T22:20:58.933 回答
3

没有通往数学的皇家之路,甚至没有通过 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]

是皇家大道还是什么?

于 2014-06-24T18:46:00.447 回答
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函数的调用中发挥作用,即在 call 中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)

同样,我们需要的“唯一”部分是sand ?

转折点:认识功能组合

让我们重新绘制上一张图,并在右侧写下我们希望每个调用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带参数x的调用是对唯一参数(作为其第二个参数)的部分应用y的(完整)应用。由于 to 的部分应用可以写为 lambda ,完全应用到它会导致 lambda ,在这种情况下我将重写为; 将单词翻译成表达式,我们得到yfxfx(\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,但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
于 2019-12-21T19:04:17.160 回答
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)

好吧,至少,这对我有帮助。即使它也不完全正确。

于 2018-12-01T10:55:01.313 回答
0

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

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 = ...
于 2018-01-21T19:38:33.390 回答
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)

Step 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)) .. )。)

Step 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个步骤,明确了foldl使用的定义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)

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

于 2020-05-04T04:30:55.830 回答