解释《小阴谋家》第 137 页的延续示例
有问题的代码是这样的:
(define multirember&co
(lambda (a lat col)
(cond
((null? lat)
(col (quote ()) (quote ())))
((eq? (car lat) a)
(multirember&co a
(cdr lat)
(lambda (newlat seen)
(col newlat
(cons (car lat) seen)))))
(else
(multirember&co a
(cdr lat)
(lambda (newlat seen)
(col (cons (car lat) newlat)
seen))))))
我整天盯着这个,但我似乎不太明白它。当您重复该函数时,您正在重新定义 col
但在示例中它们似乎使用原始定义。为什么不改变呢。如何在不传入参数 newlat
和 seen
的情况下重复它。
很难解释我的问题,因为我似乎遗漏了一块。如果有人可以给出比这本书更明确的演练,我也许能够理解它是如何工作的。
The code in question is this:
(define multirember&co
(lambda (a lat col)
(cond
((null? lat)
(col (quote ()) (quote ())))
((eq? (car lat) a)
(multirember&co a
(cdr lat)
(lambda (newlat seen)
(col newlat
(cons (car lat) seen)))))
(else
(multirember&co a
(cdr lat)
(lambda (newlat seen)
(col (cons (car lat) newlat)
seen))))))
I've stared at this all day but I can't quite seem to understand it. When you recur on the function you are re-defining col
but in the examples they seem to use the original definition. Why wouldn't it change. How can you recur on it without passing in the parameters newlat
and seen
.
It's hard to explain my question because I seem to just be missing a piece. If perhaps someone could give a more explicit walk-through than the book I may be able to understand how it works.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
让我们来看一个例子;也许这会有所帮助。 :-) 为简单起见,我将使用
list
作为收集器/延续,它将仅返回一个包含延续参数的列表。开始时,
在第一次迭代时,
(eq? (car lat) a)
条件匹配,因为lat
不为空,并且的第一个元素纬度
是'foo
。这样就设置了multirember&co
的下一个递归:在下一次迭代中,
else
匹配:因为lat
不为空,并且lat
的第一个元素是'bar
(而不是'foo
)。因此,对于下一次递归,我们有:为了便于人类阅读(并避免混淆),我们可以重命名参数(由于词法范围),而不需要对程序的语义进行任何更改:
最后,
(null ? lat)
子句匹配,因为lat
现在为空。因此,我们称其扩展为:
which(当替换
newlat1 = '()
和seen1 = '()
时)变为or (评估
(cons 'bar '( ))
)现在,替换值
newlat2 = '(bar)
和seen2 = '()
,我们得到或者,换句话说,
给出我们的最终结果
Let's step through an example; maybe that will help. :-) For simplicity, I'm just going to use
list
as the collector/continuation, which will just return a list with the arguments to the continuation.At the start,
At the first iteration, the
(eq? (car lat) a)
condition matches, sincelat
is not empty, and the first element oflat
is'foo
. This sets up the next recursion tomultirember&co
thusly:At the next iteration, the
else
matches: sincelat
is not empty, and the first element oflat
is'bar
(and not'foo
). Thus, for the next recursion, we then have:For ease of human reading (and avoid confusion), we can rename the parameters (due to lexical scoping), without any change to the program's semantics:
Finally, the
(null? lat)
clause matches, sincelat
is now empty. So we callwhich expands to:
which (when substituting
newlat1 = '()
andseen1 = '()
) becomesor (evaluating
(cons 'bar '())
)Now, substituting the values
newlat2 = '(bar)
andseen2 = '()
, we getor, in other words,
to give our final result of
我在这里找到了一个很棒的答案:
http://www.michaelharrison.ws/weblog/?p=34
我一直在努力解决这个问题也。关键是理解词法作用域(对我来说,是 Javascript)以及在 eq 和 not eq 分支上传递给 multirember&co 的内部函数。明白了这一点,你就会明白整个过程。
I found a wonderful answer here:
http://www.michaelharrison.ws/weblog/?p=34
I've been struggling through this too. The key is to understand lexical scoping (for me, à la Javascript) and the inner functions passed to multirember&co on the eq and not eq branches. Understand that, and you'll understand the entire procedure.
很长一段时间以来,我一直在努力理解
multirember&co
内部发生的事情。问题是,当我以为我已经得到它的那一刻 - 下一个任务/示例证明我还没有。对我有帮助的是整合正在发生的事情的视觉表示(对我来说,文本演练很难理解,出于某种原因)。
所以,我整理了两个流程图:
一,仅显示不同递归步骤之间的关系:
还有另一个,反映实际值:
现在,每当我感觉自己又失去了“争论的线索”时,我只要参考这个流程图,它就会让我回到正轨。
通过流程图查看“全貌”后我明白的另一件事是,
a-friend
函数只是检查是否seen
是否保存任何值(尽管它以相反的方式返回它,即当seen
中有值时#f
和#t
当saw
是空的,这可能会令人困惑PS:我 。为
evens-only*&co
做了类似的流程图,稍后出现在书。I have struggled myself, to understand what is happening inside the
multirember&co
, for quite a while. The problem is that the moment I thought I've got it - next task/example proved I have not.What helped me was putting together a visual representation of what is happening (for me text walkthroughs are tough to grasp, for some reason).
So, I have put together two flow-charts:
One, just showing the relations between different steps of recursion:
And another one, reflecting actual values:
Now, whenever I feel like I'm loosing 'the thread of an argument' again, I just refer to this flow-charts and it puts me back on track.
Another thing I've understood after looking at the 'whole picture' via flow-chart, is that
a-friend
function is simply checks whetherseen
holds any values or not (though it returns it the other way around i.e.#f
when there values inseen
and#t
whenseen
is empty, which might be confusing.P.S.: I did similar flow-charts for
evens-only*&co
, which appears later in the book.上面的链接(http://www.michaelharrison.ws/weblog/?p=34)很好地解释了整个练习是如何避免命令式编程(C,Java)需要声明两个“持有者”或“收集器” “变量(或列表、向量)显式地存在于内存中,以便在您迭代列表时捕获您的答案。通过 FP 语言方案使用延续,您在单步执行(草莓金枪鱼和剑鱼)时不会将测试结果“推送”到任何单独创建的“篮子”中;相反,当您发送适当的 consing 函数时,您将两个列表组合在一起 - 一个用于 eq?是的,另一个为 eq? false——通过重复。 。 。最后结束于第三个 col 函数,在 TLS 的第一个示例中,它是“a-friend”,它询问为保存所有匹配而构建的列表是否为空(null?)。然后,TLS 会要求您使用新的“最后”列再次“运行”multirember&co,该列仅询问包含所有“非金枪鱼”原子的列表包含多少个(“最后朋友”)。因此,有两个“一流”函数用于处理收集任务,即构建两个单独的列表,然后在递归展开结束时,原始 col(“朋友”)提出最终问题。也许“multirember&co”这个名字并不是最伟大的名字,因为它实际上不会重建列表减去要删除的原子;相反,它构建两个单独的列表(永远不会显示),然后应用最终的 col (a-friend 或 last-friend) 。 。 。它显示#t 或#f,或“非金枪鱼”列表的长度。
这是一些输出:
这是一个返回不匹配列表的列:
及其用途:
What the link above (http://www.michaelharrison.ws/weblog/?p=34) explains well is how this whole exercise is about avoiding the imperative programming (C, Java) need to declare two "holder" or "collector" variables ( or lists, vectors) explicitly in memory to catch your answers as you iterate through the list. With FP language Scheme's use of continuation, you do not "push" the test results as you step through (strawberries tuna and swordfish) into any separately created "baskets;" instead, you are consing together two lists as you send the appropriate consing functions -- one for eq? true, the other for eq? false -- through the recurs . . . finally ending up at the third col function which, in TLS's first example, is "a-friend" which asks whether the list built to hold all the matches is empty (null?). TLS then asks you to "run" multirember&co again with a new "last" col that merely asks the list containing all the "not tuna" atoms how many it contains ("last-friend"). So there are two "first class" functions being used to work with the task of collecting, i.e. building two separate lists, then at the end of the recursion unwinding, the original col ("a-friend") ask the final question. Maybe the name "multirember&co" is not the greatest name, because it really doesn't rebuild the list minus the atom to be removed; rather, it builds two separate lists -- which never get displayed -- then applies the final col (a-friend or last-friend) . . . which displays either #t or #f, or the length of the "not tuna" list.
Here's some output:
Here's a col to give back a list of non-matches:
and its use:
让我们使用一些等式伪代码,为了清楚起见,省略了一些括号(因此,我们为调用
(fxy)
编写fx y
,这是明确的):不言而喻,这是做什么的? :) 还没有? :) 用想象中的模式匹配伪代码(带有守卫)再次重写,我们有
模式匹配的语义应该非常明显:
[b, ...lat]
匹配[ 1,2,3]
,其中b = 1
和lat = [2,3]
。因此,这只是一个三例方程:当第二个参数是空列表时,“收集器”函数
col
立即被输入两个空列表作为其两个参数;当第二个参数(是一个列表)的head元素与第一个参数相同时,结果与使用以下tail递归的结果相同列表,带有修改后的收集器 - 之后它将收到两个参数,
n
和s
,--将< /em> 在当前头元素前面添加(即a
) 到s
列表,并将这两个列表提供给 this 调用的收集器函数col
;否则,在构建的收集器接收到
n
和s
后,头元素将被添加到n
列表中,并且两者都将被进一步馈送到当前收集器函数中。换句话说,我们正在处理从递归调用返回的两个结果,如果头是
a
,则将头添加到第二个结果,如果不是,则将头添加到第一个结果。因此,调用
与调用相同(将导致),
即
另一种看待它的方式是,以下是另一个等效的公式:
因此,
这是不言而喻的同一件事。
在另一个伪代码(带有列表推导)中,这表明它只
执行一次列表
lat
遍历(在原始代码中),有效地构建模仿原始列表结构的 lambda 函数的嵌套链;然后评估该链以创建两个结果,并将它们传递给最顶层的收集器函数col
。所有这些都向我们展示了Continuation-Passing Style的力量(这就是它)实际上,创建自己的函数调用协议,例如,从每个递归函数调用传回两个结果,即使通常在 lambda 演算中函数只能有一个 > 结果(即使是一对)。
Let's use some equational pseudocode with some parentheses omitted for clarity (so, we write
f x y
for the call(f x y)
, where this is unambiguous):Isn't it just self-evident, what this does? :) Not yet? :) Re-writing again with an imagined pattern-matching pseudocode (with guards), we have
The semantics of pattern matching should be quite obvious:
[b, ...lat]
matches[1,2,3]
whereb = 1
andlat = [2,3]
. This is thus just a three-cased equation:When the second argument is an empty list, the "collector" function
col
is fed two empty lists as its two arguments right away;When the second argument's (which is a list) head element is the same as the first argument, the result is the same as that for recursing with the tail of the list, with the amended collector which -- after it will receive its two arguments,
n
ands
, -- will prepend the current head element (which isa
) to thes
list, and will feed the two lists to this invocation's collector functioncol
;Otherwise, the head element will be prepended to the
n
list, aftern
ands
are received by the constructed collector, and the both will be fed further into the current collector function.In other words we're dealing with two results coming back from the recursive call, prepending the head to the second if the head was
a
, or to the first if it wasn't.Thus the call
is the same as (will result in) the call
i.e.
Another way to look at it is that the following is another, equivalent formulation:
and thus
which is self-evidently the same thing.
In yet another pseudocode (with list comprehensions), this reveals itself to be
except that only one traversal of the list
lat
is performed (in the original code), efficiently, building the nested chain of lambda functions mimicking the original list structure; which chain is then evaluated to create the two results, passing them to the top-most collector functioncol
.All this shows us the power of Continuation-Passing Style (which is what this is) to, in effect, create its own function call protocol, here for example passing back two results from each recursive function call, even though normally in lambda calculus a function can only have one result (even if, say, a pair).
该代码不会像通常那样构建解决方案,而是构建一个计算解决方案的代码,就像使用低级操作构建树一样,例如
cons
、+
、-
等,而不是使用高级累加器或过滤器。这就是为什么很难说该过程是迭代的还是递归的,因为根据迭代过程的定义,它们为本地状态使用有限量的内存。然而,这种进程使用大量内存,但这是在环境中分配的,而不是在本地参数中分配的。
首先,我在这里复制代码,以便能够在不滚动太多的情况下查看对应关系:
让我们尝试拆分问题,看看到底发生了什么。
这是一个简单的情况,它永远不会循环。
现在是有趣的案例:
在这种情况下,流程生成此代码作为结果,并最终对其进行评估。请注意,本地它仍然是尾递归的,但全局它是一个递归过程,并且它需要内存不是通过分配某些数据结构,而是通过让评估器仅分配环境帧。每个循环都会通过添加 1 个新框架来加深环境。
这构建了代码,但在另一个分支上,将结果累积到另一个变量中。
所有其他情况都是这 3 种情况中 1 种的组合,通过添加新层,每个 1 的作用就很清楚了。
The code does not build the solution, as it happens usually, but it builds a code that computes the solution, exactly as when you would build the tree using low level operations, like
cons
,+
,-
, etc, instead of using high level accumulators or filters.This is why it is difficult to say if the process is iterative or recursive, because, by the definition of the iterative processes, they use a finite amount of memory for the local state. However, this kind of process uses much memory, but this is allocated in environment, not in local parameters.
First, I duplicate the code here, to be able to see the correspondence without scrolling too much:
Let us try to split the problem to see what really happens.
This is a trivial case, it never loops.
Now the interesting cases:
In this case, the process produces this code as result, and finally evaluates it. Note that locally it is still tail-recursive, but globally it is a recursive process, and it requires memory not by allocating some data structure, but by having the evaluator allocate only environment frames. Each loop deepens the environment by adding 1 new frame.
This builds the code , but on the other branch, that accumulates a result in the other variable.
All the other cases are combinations of 1 of these 3 cases, and it is clear how each 1 acts, by adding a new layer.
我希望本演练有所帮助
正如 Chris 建议的那样,我已将 newlat/seen 重命名为 n/s 并添加了索引。
这本书给函数起了可怕的名字(a-friend new-friend Latest-fried),所以我只保留了 L(代表 lambda)和定义。
I hope this walkthrough helps
As Chris suggested, I've renamed newlat/seen to n/s and added an index.
The book gives horrible names to the functions (a-friend new-friend latest-fried), so I just kept L (for lambda) and the definition.