使用foldr实现zip
-
04-07-2019 - |
题
我目前正在使用真实世界Haskell的第4章,我正试图绕过根据foldr 实现foldl。
(这是他们的代码:)
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
,但我似乎没有取得任何进展。它甚至可能吗?
解决方案
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所示。
其他提示
这里已经给出了答案,但不是(说明性的)推导。所以即使经过这么多年,也许值得添加它。
实际上非常简单。首先,
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上首先 工作
和 ys
, f x1
r1
ys
其中 r1 =
(f x2(f x3(...(f xn z)...)))
= foldr fz [x2, X3,...,XN] 代码>
所以,使用
f x1 r1 [] = [] f x1 r1 (y1:ys1) = (x1,y1) : r1 ys1
我们通过调用 r1
安排在列表中传递信息从左到右输入列表的 rest ys1
, foldr fz [x2,x3,...,xn]
<代码> ys1 = f x2 r2
ys1
,作为下一步。就是这样。
当 ys
比 xs
(或相同长度)短时, f
的 []
情况火灾和处理停止。但如果 ys
比 xs
长,那么 f
的 []
案例将不会触发,我们将会进入最后的 f xn
z
(yn:ysn)
应用程序,
f xn z (yn:ysn) = (xn,yn) : z ysn
由于我们已经到达 xs
的末尾, zip
处理必须停止:
z _ = []
这意味着应该使用定义 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)
之后,处理将继续。
所以 r
是“当有更多 x
s&lt; 时,用更多 ys
做了什么,和 z = const []
, foldr
中的 nil
-case,是&quot;什么是当没有 x
s&quot; 时,用 ys
完成。或者 f
可以自行停止,当 ys
用尽时返回 []
。
注意 ys
如何用作一种累积值,它从 xs
列表中从左到右传递,来自 f <的一次调用/ code>到下一个(“累积”步骤,这里,从中剥离头元素)。
自然地这对应于左侧折叠,其中累积步骤是“应用函数”,使用 z = id
返回“当没有 x
s”时的最终累计值:
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
或跳过左侧折叠, foldl当t ...
,带
cons x r a = if t x then r (f a x) else r a
等
我发现了一种使用与你的方法非常相似的方法:
myzip = foldr step (const []) :: [a] -> [b] -> [(a,b)]
where step a f (b:bs) = (a,b):(f bs)
step a f [] = []
对于非本地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现在用
调用funcel->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
来压缩其参数。
这继续等......
zip
的所有这些解决方案的问题在于它们只折叠在一个列表或另一个列表上,如果它们都是“好的生产者”,这可能是一个问题,在说法中列表融合。你真正需要的是一个折叠两个列表的解决方案。幸运的是,有一篇关于这一点的文章,名为“使用超级功能进行Coroutining折叠”。
你需要一个辅助类型,一个超函数,它基本上是一个以另一个超函数作为其参数的函数。
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
运行
将所有 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 ,这在 GHC.Base模块,但可能应该更为人所知。作为一个优秀的列表生成者并使用 build
和 foldr
可以防止大量无用的生产和立即使用列表元素,并且可以进一步优化。
我自己试着理解这个优雅的解决方案,所以我试着自己推导出类型和评估。所以,我们需要编写一个函数:
zip xs ys = foldr step done xs ys
这里我们需要派生 step
和 done
,无论它们是什么。回想一下 foldr
的类型,实例化为列表:
foldr :: (a -> state -> state) -> state -> [a] -> state
但是我们的 foldr
调用必须实例化为类似下面的内容,因为我们必须接受不是一个,而是两个列表参数:
foldr :: (a -> ? -> ?) -> ? -> [a] -> [b] -> [(a,b)]
因为 - &gt;
是权利关联,所以这相当于:
foldr :: (a -> ? -> ?) -> ? -> [a] -> ([b] -> [(a,b)])
我们的([b] - &gt; [(a,b)])
对应原始 foldr
类型中的 state
类型变量签名,因此我们必须用它替换每一次出现的 state
:
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] - &gt; [(A,B)] 代码>
3`step` done
是我们的基本情况,其中 done
是一个初始值,如 foldr中的 0
(+ )0 [1..3]
。我们不希望在3之后压缩任何内容,因为3是 xs
的最终值,所以我们必须终止递归。如何在基本情况下终止递归列表?您返回空列表 []
。但回想一下 done
类型签名:
done :: [b] -> [(a,b)]
因此我们不能只返回 []
,我们必须返回一个忽略它接收的函数。因此,请使用 const 代码>
:
done = const [] -- this is equivalent to done = \_ -> []
现在让我们开始弄清楚 step
应该是什么。它将 a
类型的值与 [b] - &gt;类型的函数组合在一起。 [(a,b)]
并返回类型 [b] - &gt;的函数[(A,B)] 代码>
在 3`step` done
中,我们知道稍后将转到我们的压缩列表的结果值必须是(3,6)
(从原始<知道< code> xs
和 ys
)。因此 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
PS:如果您受到折叠优雅的启发,请阅读 使用foldr撰写foldl 然后Graham Hutton的 关于折叠的普遍性和表现力的教程 。
一种简单的方法:
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)