我很难理解第14页的The Little Schemer evens-only*&co
示例所发生的事情。
以下是代码:
(define evens-only*&co
(lambda (l col)
(cond
((null? l)
(col '() 1 0))
((atom? (car l))
(cond
((even? (car l))
(evens-only*&co (cdr l)
(lambda (newl product sum)
(col (cons (car l) newl)
(opx (car l) product)
sum))))
(else
(evens-only*&co (cdr l)
(lambda (newl product sum)
(col newl product (op+ (car l) sum)))))))
(else
(evens-only*&co (car l)
(lambda (newl product sum)
(evens-only*&co (cdr l)
(lambda (dnewl dproduct dsum)
(col (cons newl dnewl)
(opx product dproduct)
(op+ sum dsum))))))))))
最初的col
可以是:
(define evens-results
(lambda (newl product sum)
(cons sum (cons product newl))))
我没有得到的是,l
为'((1) 2 3)
,它会立即进入最终else
,(car l)
为(1)
和{(cdr l)
1}}为(2 3)
。很好,但是我的想法是空白,试图从dnewl
,dproduct
,dsum
中挑选newl
,product
,sum
。如果有人可以指导我如何设置DrRacket或Chez Scheme或MIT-Scheme来运行步进器,那将会很有帮助。
但也许我太早了。是否有任何初学者第一次读到这个实际上应该理解这种狂野的延续?
答案 0 :(得分:20)
我发现这一节在第一次阅读时也很混乱,只是在我读完其他关于延续和延续传递风格的事后才开始得到它(这就是这个)。
冒着解释你已经得到的东西的风险,一种看待它的方法有助于我将“收集器”或“延续”视为取代函数返回值的正常方式。在正常的编程风格中,您可以调用函数,接收值,并在调用者中对其执行某些操作。例如,标准递归length
函数包含非空案例的表达式(+ 1 (length (cdr list)))
。这意味着一旦(length (cdr list))
返回一个值,就会有一个计算等待它产生的任何值,我们可以将其视为(+ 1 [returned value])
。在正常编程中,解释器会跟踪这些待处理的计算,这些计算往往会“叠加”,正如您在本书的前几章中所看到的那样。例如,在递归地计算列表的长度时,我们有一个“等待计算”的嵌套,因为列表很长,所以有很多级别。
在延续传递样式中,我们告诉函数在生成其值时通过为其提供“延续”而不是调用函数并使用返回的结果“打电话。 (这类似于您在异步Javascript编程中使用回调所做的事情,例如:您编写result = someFunction();
而不是编写someFunction(function (result) { ... })
,而使用result
的所有代码都在内部回调函数)。
此处为继续传递样式的length
,仅供比较。我已经调用了continuation参数return
,它应该在这里建议它如何起作用,但请记住它只是一个普通的Scheme变量,就像其他任何变量一样。 (通常,continuation参数在此样式中称为k
。)
(define (length/k lis return)
(cond ((null? lis) (return 0))
(else
(length/k (cdr lis)
(lambda (cdr-len)
(return (+ cdr-len 1)))))))
在an article on continuations by Little Schemer co-author Dan Friedman中阅读此类代码有一个有用的提示。 (参见第8页开始的第II-5节)。解释,这是上面的else
条款所说的:
假设您在
length/k
上调用(cdr lis)
的结果,并且 称之为cdr-len
,然后添加一个并传递此添加的结果 继续(return
)。
请注意,这几乎就是解释器在评估函数正常版本中的(+ 1 (length (cdr lis)))
时必须做的事情(除了它不必为中间结果(length (cdr lis))
命名通过传递延续或回调,我们使控制流(以及中间值的名称)显式化,而不是让解释器跟踪它。
让我们将此方法应用于evens-only*&co
中的每个子句。这里有点复杂,因为这个函数产生三个值而不是一个:删除奇数的嵌套列表;偶数的乘积;和奇数的总和。这是第一个子句,其中(car l)
已知为偶数:
(evens-only*&co (cdr l)
(lambda (newl product sum)
(col (cons (car l) newl)
(opx (car l) product)
sum)))
想象一下你有删除奇数的结果, 乘以平均值,并从列表的
cdr
中添加奇数, 并分别称他们为newl
,product
和sum
。cons
列表的头部newl
(因为它是偶数,它应该去 在结果中);将product
乘以列表的头部(因为 我们正在计算平均值的产品);只留下sum
;并通过这些 等待继续col
的三个值。
以下是列表头部为奇数的情况:
(evens-only*&co (cdr l)
(lambda (newl product sum)
(col newl product (op+ (car l) sum))))
和以前一样,但是将newl
和product
的相同值传递给延续(即“返回”它们),以及sum
和列表头部的总和因为我们总结了奇数。
这是最后一个,其中(car l)
是一个嵌套列表,并且由于双递归而略显复杂:
(evens-only*&co (car l)
(lambda (newl product sum)
(evens-only*&co (cdr l)
(lambda (dnewl dproduct dsum)
(col (cons newl dnewl)
(opx product dproduct)
(op+ sum dsum))))))
想象一下,你有删除,求和和添加的结果
(car l)
中的数字并调用这些newl
,product
和sum
;然后 想象一下你对(cdr l)
做同样的事情有结果, 并称他们为dnewl
,dproduct
和dsum
。等你 继续,给出cons
newl
和dnewl
生成的值 (因为我们正在制作一份清单清单);相乘product
和dproduct
;并添加sum
和dsum
。
注意:每次进行递归调用时,我们都会为递归调用构造一个新的延续,它会“关闭”参数的当前值l
和返回延续 - {{1}换句话说,你可以想象我们在递归过程中建立的连续链,就像建模一个更常规编写的函数的“调用堆栈”一样!
希望能为你的问题提供部分答案。如果我有点过分了,那只是因为我认为,在递归本身之后,延续是 The Little Schemer 中第二个非常整洁,思想扩展的想法和一般的编程。
答案 1 :(得分:1)
answer Jon O.是对基本概念的非常深入的解释。虽然对我来说(并且希望对其他人也是如此),但是当他们有视觉表现时,理解这样的概念要容易得多。
所以,我准备了两个流程图(类似于ones I did for multirember&co
,解决了在调用evens-only*&co
给定l
是:
'((9 1 2 8) 3 10 ((9 9) 7 6) 2)
和col
是:
(define the-last-friend
(lambda (newl product sum)
(cons sum (cons product newl))
)
)
一个流程图,反映变量在递归的不同步骤中的关系: 第二个流程图,显示实际值,正在传递:
我的希望是,这个答案将成为Jon's explanation above的一个不错的补充。
答案 2 :(得分:0)
我一直在阅读如何设计程序(felleisen et.al.)。我正在浏览他们定义本地定义的部分。我编写了一个使用本地定义实现上述evens-only& co的代码。这是我写的:
(define (evens-only&co l)
(local ((define (processing-func sum prod evlst lst)
(cond ((null? lst) (cons sum (cons prod evlst)))
((atom? (car lst))
(cond ((even? (car lst)) (processing-func sum (* prod (car lst)) (append evlst (list (car lst))) (cdr lst)))
(else
(processing-func (+ sum (car lst)) prod evlst (cdr lst)))))
(else
(local ((define inner-lst (processing-func sum prod '() (car lst))))
(processing-func (car inner-lst) (cadr inner-lst) (append evlst (list (cddr inner-lst))) (cdr lst)))))))
(processing-func 0 1 '() l)))
为了进行测试,当我进入(evens-only& co'((9 1 2 8)3 10((9 9)7 6)2))时,它返回'(38 1920(2 8)10(( )6)2)正如小阴谋家所料。但是,我的代码在一个条件下失败:当根本没有偶数时,evens的乘积仍显示为1.例如(evens-only& co'((9 1)3((9 9)7) ))返回'(38 1()(()))。我想我需要一个额外的功能来纠正这个问题。 @melwasul:如果您不熟悉本地定义,很抱歉在此发布。我建议你也阅读HTDP。这对初学者来说是一本很好的书。 但是作为计划专家的人也可以在我的代码上发表评论。我对本地定义的理解是否正确?
答案 3 :(得分:0)
在等式伪代码(一个KRC - 表示法中,为f x y
编写(f x y)
,这是明确的,这是
evens-only*&co l col
= col [] 1 0 , IF null? l
= evens-only*&co (cdr l)
( newl product sum =>
col (cons (car l) newl)
(opx (car l) product)
sum ) , IF atom? (car l) && even? (car l)
= evens-only*&co (cdr l)
( newl product sum =>
col newl product (op+ (car l) sum) ) , IF atom? (car l)
= evens-only*&co (car l)
( anewl aproduct asum =>
evens-only*&co (cdr l)
( dnewl dproduct dsum =>
col (cons anewl dnewl)
(opx aproduct dproduct)
(op+ asum dsum) ) ) , OTHERWISE
这是一个CPS代码,它在保留树结构的同时从输入嵌套列表(即树)中收集所有均值,并且还找到所有均值的乘积;至于非均衡,它总结:
如果l
是一个空列表,则三个基本(标识)值作为参数传递给col;
如果(car l)
是偶数,则处理(cdr l)
的结果为newl
,product
和sum
,和然后将它们作为参数传递给col
,而前两个则通过使用(car l)
(偶数)来增加/增加;
如果(car l)
是非偶数的原子,则处理(cdr l)
的结果为newl
,product
和sum
,然后它们作为参数传递给col
,第三个通过与(car l)
(非偶数原子)求和来增加;
如果(car l)
是列表,则处理(car l)
的结果为anewl
,aproduct
和asum
,,然后处理(cdr l)
的结果为dnewl
,dproduct
和dsum
,然后三个组合结果作为参数传递到col
。
[]
,1
和0
分别是monoids列表的标识元素,乘法下的数字和加法下的数字。这只是意味着在结合到结果中时不会改变结果的特殊值。
作为一个例子,对于'((5) 2 3 4)
(与问题中的示例接近),它会创建计算
evens-only*&co [[5], 2, 3, 4] col
=
col (cons [] ; original structure w only the evens kept in,
(cons 2 ; for the car and the cdr parts
(cons 4 [])))
(opx 1 ; multiply the products of evens in the car and
(opx 2 (opx 4 1))) ; in the cdr parts
(op+ (op+ 5 0) ; sum, for the non-evens
(op+ 3 0))
类似于my other answer(对于姐妹问题),这是另一种写这个的方式,带有模式匹配的伪代码(带有警卫):
evens-only*&co = g where
g [a, ...xs...] col
| pair? a = g a ( la pa sa =>
g xs ( ld pd sd =>
col [la, ...ld...] (* pa pd) (+ sa sd) ) )
| even? a = g xs ( l p s => col [ a, ...l... ] (* a p ) s )
| otherwise = g xs ( l p s => col l p (+ a s ) )
g [] col = col [] 1 0
这种符号的经济性(和多样性)确实使得它更加清晰,更容易只看到而不是迷失在长期名称沙拉中的功能和变量等,而且parens重载作为列表数据的句法分隔符,子句分组(如cond
表达式中),名称绑定(在lambda
表达式中)和函数调用指示符所有正在完全相似< / em>的。 S表达式符号的相同均匀性有利于机器操作的简便性(即lisp的 read
和宏)是对人类可读性的不利影响它的。