使用foldr实现zip
我目前正在阅读 Real World 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 ,但我似乎没有取得任何进展。 有可能吗?
I'm currently on chapter 4 of Real World Haskell, and I'm trying to wrap my head around implementing foldl in terms of foldr.
(Here's their code:)
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)
I thought I'd try to implement zip
using the same technique, but I don't seem to be making any progress. Is it even possible?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
这是如何工作的:(foldr 步骤完成 xs) 返回一个消耗的函数
是的; 所以我们沿着 xs 列表构建一个嵌套组合
每个函数都将应用于 ys 的相应部分。
如何想出它:我从总体想法开始(来自类似的
之前见过的例子),
然后依次填写以下每一行所需的内容
是为了使类型和值正确。 这是最容易的
首先考虑最简单的情况,然后再考虑更困难的情况。
第一行可以写得更简单,如
马蒂亚斯特所示。
How this works: (foldr step done xs) returns a function that consumes
ys; so we go down the xs list building up a nested composition of
functions that will each be applied to the corresponding part of ys.
How to come up with it: I started with the general idea (from similar
examples seen before), wrote
then filled in each of the following lines in turn with what it had to
be to make the types and values come out right. It was easiest to
consider the simplest cases first before the harder ones.
The first line could be written more simply as
as mattiast showed.
这里已经给出了答案,但不是(说明性的)推导。 所以即使过了这么多年,也许还是值得添加它。
其实很简单。 首先,
因此通过 eta 扩展,
正如这里显而易见的,如果
f
在其第二个参数中是非强制的,它首先开始工作在x1
和ys
上,f x1
r1
ys
其中r1 =
(f x2 (f x3 (... (f xn z) ...)))
=折叠 fz [x2,x3,...,xn]
。因此,
我们通过调用
r1
来沿着列表从左到右排列信息的传递输入列表的其余部分ys1
,foldr fz [x2,x3,...,xn]
< code>ys1 = f x2r2
ys1
,作为下一步。 就是这样。当f xn
ys
短于xs
(或相同长度)时,f
的[]
情况会触发,并且处理停止。 但是,如果ys
比xs
长,那么f
的[]
案例将不会触发,我们将到达最终的z
(yn:ysn)
应用程序,因为我们已经到达了末尾
xs
,zip
处理必须停止:这意味着应使用定义
z = const []
:从
f
的角度来看,r
扮演着成功延续的角色,f
在处理完成时调用它。发出(x,y)
对后继续。所以
r
是“当有更多x
s时,更多ys
会做什么”,并且z = const []
,foldr
中的nil
情况,是“使用完成的操作ys
当不再有x
s" 时。 或者,f
可以自行停止,当ys
耗尽时返回[]
。请注意
ys
如何用作一种累加值,该值通过一次f
调用沿列表xs
从左向右传递到下一步(这里的“累积”步骤是从中剥离头元素)。自然这对应于左侧折叠,其中累积步骤是“应用函数”,其中
z = id
当“不再有x
s”时返回最终的累加值:同样,对于有限列表,
由于组合函数需要决定是否继续,所以它现在可以有可以提前停止的左折叠:
或跳过左折叠,
foldlWhen t ...
,等等。
The answer had already been given here, but not an (illustrative) derivation. So even after all these years, perhaps it's worth adding it.
It is actually quite simple. First,
hence by eta-expansion,
As is apparent here, if
f
is non-forcing in its 2nd argument, it gets to work first onx1
andys
,f x1
r1
ys
wherer1 =
(f x2 (f x3 (... (f xn z) ...)))
= foldr f z [x2,x3,...,xn]
.So, using
we arrange for passage of information left-to-right along the list, by calling
r1
with the rest of the input listys1
,foldr f z [x2,x3,...,xn]
ys1 = f x2
r2
ys1
, as the next step. And that's that.When
ys
is shorter thanxs
(or the same length), the[]
case forf
fires and the processing stops. But ifys
is longer thanxs
thenf
's[]
case won't fire and we'll get to the finalf xn
z
(yn:ysn)
application,Since we've reached the end of
xs
, thezip
processing must stop:And this means the definition
z = const []
should be used:From the standpoint of
f
,r
plays the role of a success continuation, whichf
calls when the processing is to continue, after having emitted the pair(x,y)
.So
r
is "what is done with moreys
when there are morex
s", andz = const []
, thenil
-case infoldr
, is "what is done withys
when there are no morex
s". Orf
can stop by itself, returning[]
whenys
is exhausted.Notice how
ys
is used as a kind of accumulating value, which is passed from left to right along the listxs
, from one invocation off
to the next ("accumulating" step being, here, stripping a head element from it).Naturally this corresponds to the left fold, where an accumulating step is "applying the function", with
z = id
returning the final accumulated value when "there are no morex
s":Similarly, for finite lists,
And since the combining function gets to decide whether to continue or not, it is now possible to have left fold that can stop early:
or a skipping left fold,
foldlWhen t ...
, withetc.
我找到了一种与你的方法非常相似的方法:
I found a way using quite similar method to yours:
对于这里的非本地 Haskellers,我编写了该算法的一个方案版本,以使其更清楚实际发生的情况:
foldr
产生一个函数,当应用于列表时,该函数将返回列表的 zip 与提供给函数的列表折叠在一起。 由于惰性求值,Haskell 隐藏了内部 lambda 。进一步分解:
对输入进行压缩:'(1 2 3)
Foldr 函数被调用,
This 扩展为:
如果我们现在返回 this,我们就会有一个函数,它接受一个元素的列表
并返回对(3 个元素):
继续,foldr 现在调用 func with
This is a func,它接受一个包含两个元素的列表,现在,并用
(list 2 3)
压缩它们:发生了什么?
在本例中,
a
是(list 9 1)
并且,您还记得,
f
用3
压缩其参数代码>.这继续等等......
For the non-native Haskellers here, I've written a Scheme version of this algorithm to make it clearer what's actually happening:
The
foldr
results in a function which, when applied to a list, will return the zip of the list folded over with the list given to the function. The Haskell hides the innerlambda
because of lazy evaluation.To break it down further:
Take zip on input: '(1 2 3)
The foldr func gets called with
This expands to:
If we were to return this now, we'd have a function which takes a list of one element
and returns the pair (3 element):
Continuing, foldr now calls func with
This is a func which takes a list with two elements, now, and zips them with
(list 2 3)
:What's happening?
a
, in this case, is(list 9 1)
And, as you recall,
f
zips its argument with3
.And this continues etc...
所有这些 zip 解决方案的问题在于它们仅折叠一个列表或另一个列表,如果它们都是“优秀的生产者”(用列表融合的说法),这可能会成为问题。 您真正需要的是一个折叠两个列表的解决方案。 幸运的是,有一篇论文正是关于这一点的,名为“Coroutining Folds with Hyperfunctions”。
您需要一个辅助类型,即超函数,它基本上是一个以另一个超函数作为参数的函数。
这里使用的超函数基本上就像普通函数的“堆栈”一样。
您还需要一种将两个超功能端到端地组合在一起的方法。
这与法律上的
push
相关:这原来是一个关联运算符,这就是恒等式:
您还需要一些东西,可以忽略“堆栈”上的其他所有内容并返回特定值:
最后,您需要一种从超函数中获取值的方法:
run
将所有push
函数端到端地串在一起,直到它遇到>base
或无限循环。因此,现在您可以使用将信息从一个列表传递到另一个列表的函数,将两个列表折叠为超函数,并组合最终值。
折叠两个列表很重要的原因是 GHC 所做的称为“列表融合”的事情,这在 GHC.Base 模块,但可能应该更知名。 成为一个好的列表生成器并将
build
与foldr
一起使用可以防止大量无用的生成和列表元素的立即消耗,并且可以公开进一步的优化。The problem with all these solutions for
zip
is that they only fold over one list or the other, which can be a problem if both of them are "good producers", in the parlance of list fusion. What you actually need is a solution that folds over both lists. Fortunately, there is a paper about exactly that, called "Coroutining Folds with Hyperfunctions".You need an auxiliary type, a hyperfunction, which is basically a function that takes another hyperfunction as its argument.
The hyperfunctions used here basically act like a "stack" of ordinary functions.
You also need a way to put two hyperfunctions together, end to end.
This is related to
push
by the law:This turns out to be an associative operator, and this is the identity:
You also need something that disregards everything else on the "stack" and returns a specific value:
And finally, you need a way to get a value out of a hyperfunction:
run
strings all of thepush
ed functions together, end to end, until it hits abase
or loops infinitely.So now you can fold both lists into hyperfunctions, using functions that pass information from one to the other, and assemble the final value.
The reason why folding over both lists matters is because of something GHC does called list fusion, which is talked about in the GHC.Base module, but probably should be much more well-known. Being a good list producer and using
build
withfoldr
can prevent lots of useless production and immediate consumption of list elements, and can expose further optimizations.我试图自己理解这个优雅的解决方案,所以我尝试自己推导类型和评估。 因此,我们需要编写一个函数:
这里我们需要导出
step
和done
,无论它们是什么。 回想一下foldr
的类型,实例化为列表:但是,我们的
foldr
调用必须实例化为如下所示的内容,因为我们必须接受的不是一个,而是两个列表参数:因为
-> ;
是右关联,这相当于:我们的
([b] -> [(a,b)])
对应于原始foldr
类型签名中的state
类型变量,因此我们必须替换每次出现的state< /code> :
这意味着我们传递给
foldr
的参数必须具有以下类型:回想一下
foldr (+) 0 [1,2,3]
扩展至:因此,如果
xs = [1,2,3]
和ys = [4,5,6,7]
,我们的foldr
调用将扩展为:这意味着我们的
1 `step` (2 `step` (3 `step` did))
构造必须创建一个递归函数,该函数将遍历[4,5, 6,7]
并压缩元素。 (请记住,如果原始列表之一较长,则多余的值将被丢弃)。 IOW,我们的构造必须具有类型[b] -> [(a,b)]
。3 `step` done
是我们的基本情况,其中done
是初始值,如foldr (+) 0 [ 中的
。 我们不想压缩 3 之后的任何内容,因为 3 是 xs 的最终值,因此我们必须终止递归。 在基本情况下如何终止列表上的递归? 您返回空列表0
1..3][]
。 但回想一下done
类型签名:因此我们不能只返回
[]
,我们必须返回一个忽略它接收到的任何内容的函数。 因此使用const
:
现在让我们开始弄清楚
step
应该是什么。 它将a
类型的值与[b] -> 类型的函数组合在一起。 [(a,b)]
并返回[b] -> 类型的函数 [(a,b)]
。在
3`step`done
中,我们知道稍后将进入我们的压缩列表的结果值必须是(3,6)
(从原始xs得知
和ys
)。 因此,3 `step` did
必须计算为:记住,我们必须返回一个函数,在其中我们以某种方式压缩元素,上面的代码是有意义的和类型检查的。
现在我们假设了
step
应该如何评估,让我们继续评估。 以下是我们的foldr
评估中的所有归约步骤的样子:评估产生了该步骤的实现(请注意,我们通过返回一个值来提前耗尽元素。空列表):
因此,完整的函数
zip
实现如下:PS:如果您受到优雅的折叠的启发,请阅读 使用foldr编写foldl,然后使用Graham Hutton的关于折叠的通用性和表现力的教程。
I tried to understand this elegant solution myself, so I tried to derive the types and evaluation myself. So, we need to write a function:
Here we need to derive
step
anddone
, whatever they are. Recallfoldr
's type, instantiated to lists:However our
foldr
invocation must be instantiated to something like below, because we must accept not one, but two list arguments:Because
->
is right-associative, this is equivalent to:Our
([b] -> [(a,b)])
corresponds tostate
type variable in the originalfoldr
type signature, therefore we must replace every occurrence ofstate
with it:This means that arguments that we pass to
foldr
must have the following types:Recall that
foldr (+) 0 [1,2,3]
expands to:Therefore if
xs = [1,2,3]
andys = [4,5,6,7]
, ourfoldr
invocation would expand to:This means that our
1 `step` (2 `step` (3 `step` done))
construct must create a recursive function that would go through[4,5,6,7]
and zip up the elements. (Keep in mind, that if one of the original lists is longer, the excess values are thrown away). IOW, our construct must have the type[b] -> [(a,b)]
.3 `step` done
is our base case, wheredone
is an initial value, like0
infoldr (+) 0 [1..3]
. We don't want to zip anything after 3, because 3 is the final value ofxs
, so we must terminate the recursion. How do you terminate the recursion over list in the base case? You return empty list[]
. But recalldone
type signature:Therefore we can't return just
[]
, we must return a function that would ignore whatever it receives. Therefore useconst
:Now let's start figuring out what
step
should be. It combines a value of typea
with a function of type[b] -> [(a,b)]
and returns a function of type[b] -> [(a,b)]
.In
3 `step` done
, we know that the result value that would later go to our zipped list must be(3,6)
(knowing from originalxs
andys
). Therefore3 `step` done
must evaluate into:Remember, we must return a function, inside which we somehow zip up the elements, the above code is what makes sense and typechecks.
Now that we assumed how exactly
step
should evaluate, let's continue the evaluation. Here's how all reduction steps in ourfoldr
evaluation look like:The evaluation gives rise to this implementation of step (note that we account for
ys
running out of elements early by returning an empty list):Thus, the full function
zip
is implemented as follows:P.S.: If you are inspired by elegance of folds, read Writing foldl using foldr and then Graham Hutton's A tutorial on the universality and expressiveness of fold.
一个简单的方法:
A simple approach: