使用foldr实现zip

时间:2008-10-24 20:27:52

标签: haskell functional-programming fold combinators

我目前正在使用真实世界Haskell的第4章,我正试图绕过implementing foldl in terms of foldr

(这是他们的代码:)

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)

我以为我会尝试使用相同的技术来实现zip,但我似乎没有取得任何进展。它甚至可能吗?

7 个答案:

答案 0 :(得分:16)

zip2 xs ys = foldr step done xs ys
  where done ys = []
        step x zipsfn []     = []
        step x zipsfn (y:ys) = (x, y) : (zipsfn ys)

这是如何工作的:(foldr step done xs)返回一个消耗的函数 YS;所以我们沿着xs列表构建一个嵌套的组合 将各自应用于ys的相应部分的函数。

如何提出它:我从一般的想法开始(来自类似的 以前见过的例子),写道

zip2 xs ys = foldr step done xs ys

然后依次用以下内容填写以下每一行 要使类型和价值观正确。这是最容易的 在较难的案例之前先考虑最简单的案例。

第一行可以更简单地写成

zip2 = foldr step done

正如mattiast所示。

答案 1 :(得分:10)

这里已经给出了答案,但不是(说明性的)推导。所以即使经过这么多年,也许值得添加它。

实际上非常简单。首先,

foldr f z xs 
   = foldr f z [x1,x2,x3,...,xn] = f x1 (foldr f z [x2,x3,...,xn])
   = ... = f x1 (f x2 (f x3 (... (f xn z) ...)))

因此通过eta-expansion,

foldr f z xs ys
   = foldr f z [x1,x2,x3,...,xn] ys = f x1 (foldr f z [x2,x3,...,xn]) ys
   = ... = f x1 (f x2 (f x3 (... (f xn z) ...))) ys

正如此处显而易见的那样,如果f在其第二个参数中是非强制的,则它会在x1和<{1}}上开始 {1}},ys f x1 r1其中ys r1 = (f x2 (f x3 (... (f xn z) ...)))

所以,使用

f x1 r1 [] = []
f x1 r1 (y1:ys1) = (x1,y1) : r1 ys1

我们通过使用 = foldr f z [x2,x3,...,xn] 来安排从列表中传递信息从左到右 >输入列表的{rest r1 ys1 foldr f z [x2,x3,...,xn] ys1 = f x2 r2 ,作为下一步。那是那个。


ys1短于ys(或相同的长度)时,xs的{​​{1}}案例会触发,处理将停止。但如果[]超过f,那么ys xs案件就会被解雇,我们将会进入最后f [] 1}} f xn z申请,

f xn z (yn:ysn) = (xn,yn) : z ysn

由于我们已到达(yn:ysn)的末尾, xs 处理必须停止:

z _ = []

这意味着应该使用定义zip

z = const []

zip xs ys = foldr f (const []) xs ys where f x r [] = [] f x r (y:ys) = (x,y) : r ys 的角度来看,f扮演成功延续的角色,r在发出处理后继续处理对f

所以(x,y)&#34;当r s&#34; 和{{1}更多ys时,更多x会怎样做? } z = const []中的 nil -case是&#34;当foldr不再有ys时,{em } S&#34; 。或x可以自行停止,f耗尽时返回[]


请注意ys如何用作一种累积值,它从列表ys从左向右传递,从xs的一次调用传递到下一个(&# 34;积累&#34;步骤,这里,剥离头部元素)。

Naturally这对应于左侧折叠,其中累积步骤为&#34;应用函数&#34;,f返回最终累计值时为&#34;没有更多z = id s&#34;:

x

同样,对于有限列表,

foldl f a xs =~ foldr (\x r a-> r (f a x)) id xs a

由于组合功能决定是否继续,现在可以让左侧折叠可以提前停止:

foldr f a xs =~ foldl (\r x a-> r (f x a)) id xs a

或跳过左侧折叠,foldlWhile t f a xs = foldr cons id xs a where cons x r a = if t x then r (f a x) else a

foldlWhen t ...

答案 2 :(得分:9)

我发现了一种使用与你的方法非常相似的方法:

myzip = foldr step (const []) :: [a] -> [b] -> [(a,b)]
    where step a f (b:bs) = (a,b):(f bs)
          step a f [] = []

答案 3 :(得分:5)

对于非本地Haskellers,我已经编写了这个算法的Scheme版本,以便更清楚实际发生的事情:

> (define (zip lista listb)
    ((foldr (lambda (el func)
           (lambda (a)
             (if (empty? a)
                 empty
                 (cons (cons el (first a)) (func (rest a))))))
         (lambda (a) empty)
         lista) listb))
> (zip '(1 2 3 4) '(5 6 7 8))
(list (cons 1 5) (cons 2 6) (cons 3 7) (cons 4 8))

foldr会产生一个函数,当应用于列表时,它将返回折叠列表的zip,其中包含给函数的列表。由于懒惰的评估,Haskell隐藏了内部lambda


进一步分解:

输入拉链:'(1 2 3) 使用

调用foldr函数
el->3, func->(lambda (a) empty)

这扩展为:

(lambda (a) (cons (cons el (first a)) (func (rest a))))
(lambda (a) (cons (cons 3 (first a)) ((lambda (a) empty) (rest a))))

如果我们现在要返回这个,我们有一个函数,它接受一个元素的列表 并返回该对(3个元素):

> (define f (lambda (a) (cons (cons 3 (first a)) ((lambda (a) empty) (rest a)))))
> (f (list 9))
(list (cons 3 9))

继续,foldr现在用

调用func
el->3, func->f ;using f for shorthand
(lambda (a) (cons (cons el (first a)) (func (rest a))))
(lambda (a) (cons (cons 2 (first a)) (f (rest a))))

这是一个func,它现在带有两个元素的列表,并用(list 2 3)拉上它们:

> (define g (lambda (a) (cons (cons 2 (first a)) (f (rest a)))))
> (g (list 9 1))
(list (cons 2 9) (cons 3 1))

发生了什么事?

(lambda (a) (cons (cons 2 (first a)) (f (rest a))))
在这种情况下,

a(list 9 1)

(cons (cons 2 (first (list 9 1))) (f (rest (list 9 1))))
(cons (cons 2 9) (f (list 1)))

而且,正如您记得的那样,f将其论点与3拉开。

这继续等......

答案 4 :(得分:5)

zip的所有这些解决方案的问题在于它们只折叠在一个列表或另一个列表上,如果它们都是“好的生产者”,这可能是一个问题,就像列表融合的说法一样。你真正需要的是一个折叠两个列表的解决方案。幸运的是,有一篇文章正是如此,称为"Coroutining Folds with Hyperfunctions"

你需要一个辅助类型,一个超函数,它基本上是一个以另一个超函数作为其参数的函数。

newtype H a b = H { invoke :: H b a -> b }

这里使用的超级函数基本上就像普通函数的“堆栈”。

push :: (a -> b) -> H a b -> H a b
push f q = H $ \k -> f $ invoke k q

你还需要一种方法将两个超级功能放在一起,端到端。

(.#.) :: H b c -> H a b -> H a c
f .#. g = H $ \k -> invoke f $ g .#. k

这与法律push有关:

(push f x) .#. (push g y) = push (f . g) (x .#. y)

这结果是一个关联运算符,这是标识:

self :: H a a
self = H $ \k -> invoke k self

您还需要忽略“堆栈”上其他所有内容并返回特定值的内容:

base :: b -> H a b
base b = H $ const b

最后,您需要一种从超级函数中获取值的方法:

run :: H a a -> a
run q = invoke q self

run将所有push ed函数串起来,端到端,直到它遇到base或无限循环。

现在,您可以将两个列表折叠为超级函数,使用将信息从一个传递到另一个的函数,并汇总最终值。

zip xs ys = run $ foldr (\x h -> push (first x) h) (base []) xs .#. foldr (\y h -> push (second y) h) (base Nothing) ys where
  first _ Nothing = []
  first x (Just (y, xys)) = (x, y):xys

  second y xys = Just (y, xys)

折叠两个列表之所以重要的原因是因为GHC所谓的 list fusion ,这在the GHC.Base module中被讨论过,但可能应该更为人所知。作为一个好的列表生成者并使用buildfoldr一起使用可以防止大量无用的生产和立即使用列表元素,并且可以进一步优化。

答案 5 :(得分:2)

我自己试着理解这个优雅的解决方案,所以我试着自己推导出类型和评估。所以,我们需要编写一个函数:

zip xs ys = foldr step done xs ys

我们需要派生stepdone,无论它们是什么。调用foldr的类型,实例化为列表:

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

但是我们的foldr调用必须实例化为类似下面的内容,因为我们必须接受不是一个,而是两个列表参数:

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

由于->right-associative,因此相当于:

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

我们的([b] -> [(a,b)])对应原始state类型签名中的foldr类型变量,因此我们必须将state的每一个匹配项替换为:{/ p>

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

这意味着我们传递给foldr的参数必须具有以下类型:

step :: a -> ([b] -> [(a,b)]) -> [b] -> [(a,b)]
done :: [b] -> [(a,b)]
xs :: [a]
ys :: [b]

回想一下,foldr (+) 0 [1,2,3]扩展为:

1 + (2 + (3 + 0))

因此,如果xs = [1,2,3]ys = [4,5,6,7],我们的foldr调用会扩展为:

1 `step` (2 `step` (3 `step` done)) $ [4,5,6,7]

这意味着我们的1 `step` (2 `step` (3 `step` done))构造必须创建一个递归函数,该函数将通过[4,5,6,7]并压缩元素。 (请记住,如果其中一个原始列表较长,则会丢弃多余的值)。 IOW,我们的构造必须具有类型[b] -> [(a,b)]

3 `step` done是我们的基本案例,其中done是初始值,例如0中的foldr (+) 0 [1..3]。我们不想在3之后压缩任何内容,因为3是xs的最终值,所以我们必须终止递归。如何在基本情况下终止递归列表?您返回空列表[]。但请回忆done类型签名:

done :: [b] -> [(a,b)]

因此我们不能只返回[],我们必须返回一个忽略它接收的函数。因此,请使用const

done = const [] -- this is equivalent to done = \_ -> []

现在让我们开始弄清step应该是什么。它将类型a的值与类型[b] -> [(a,b)]的函数组合在一起,并返回类型为[b] -> [(a,b)]的函数。

3 `step` done中,我们知道稍后转到我们的压缩列表的结果值必须是(3,6)(从原始xsys了解)。因此3 `step` done必须评估为:

\(y:ys) -> (3,y) : done ys

请记住,我们必须返回一个函数,在其中我们以某种方式压缩元素,上面的代码是有意义的和typechecks。

现在我们假设step应该如何评估,让我们继续评估。以下是foldr评估中所有缩减步骤的显示方式:

3 `step` done -- becomes
(\(y:ys) -> (3,y) : done ys)
2 `step` (\(y:ys) -> (3,y) : done ys) -- becomes
(\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys)
1 `step` (\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys) -- becomes
(\(y:ys) -> (1,y) : (\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys) ys)

评估产生了这个步骤的实现(请注意,我们通过返回空列表来解释早期用尽的ys):

step x f = \[] -> []
step x f = \(y:ys) -> (x,y) : f ys

因此,完整函数zip实现如下:

zip :: [a] -> [b] -> [(a,b)]
zip xs ys = foldr step done xs ys
  where done = const []
        step x f [] = []
        step x f (y:ys) = (x,y) : f ys

P.S。:如果您受到褶皱优雅的启发,请阅读Writing foldl using foldr然后阅读Graham Hutton的A tutorial on the universality and expressiveness of fold

答案 6 :(得分:0)

一种简单的方法:

lZip, rZip :: Foldable t => [b] -> t a -> [(a, b)]

-- implement zip using fold?
lZip xs ys = reverse.fst $ foldl f ([],xs) ys
     where f  (zs, (y:ys)) x = ((x,y):zs, ys)

-- Or;
rZip xs ys = fst $ foldr f ([],reverse xs) ys
     where f x (zs, (y:ys))  = ((x,y):zs, ys)