我使用DrRacket调试模式逐步在p.34和p.37上运行这两个示例。以下是两个示例中第一次处理(cdr lat)
时的堆栈窗口结果。
p.34,没有cons
(define rember
(lambda (a lat)
(cond
((null? lat) '())
(else (cond
((eq? a (car lat)) (cdr lat))
(else (rember a (cdr lat)))
)))))
(rember 'c '(a b c d))
调试器中的堆栈区域:
(cdr ...)
(rember ...)
第37页,最后一行cons
:
(define rember
(lambda (a lat)
(cond
((null? lat) '())
(else (cond
((eq? a (car lat)) (cdr lat))
(else (cons (car lat)
(rember a (cdr lat)))))))))
(rember 'c '(a b c d))
调试器中的堆栈区域:
(cdr ...)
(rember ...)
(rember ...)
带有p.37代码的堆栈区域表示在处理rember
之前,(cdr lat)
的第二次调用已被归类为 unknown 。
两个例子的唯一区别是p.37添加了“cons
”。 Cons需要2个参数,一个s表达式和一个列表。
如果没有(cdr lat)
,rember
本身不会返回列表。在本书前40页中包含(cdr lat)
的所有示例都具有相同的(function (cdr variable)
格式。
我不明白为什么p.37示例rember
本身被识别为 unknown 并证明待处理的减少,而且可以处理包含的(cdr lat)
。
或者rember
的第二个参数位置cons
被解释的原因。
谢谢!
答案 0 :(得分:2)
让我强烈建议你在这里使用步进器,而不是调试器。我想你会看到一套更加一致的减少规则。具体来说,我认为你不会看到任何“被识别为未知的东西。”
要使用步进器:打开一个新的缓冲区,确保语言级别设置为带有列表缩写的开始学生,并将定义和调用粘贴到定义窗口中。点击“步骤”。我想你会很快看到两个评估之间的差异。
如果没有任何意义,请询问后续问题!
答案 1 :(得分:2)
TL; DR:你看到(和误解)的是函数调用的堆栈,以及尾递归的影响。 / p>
回答有关调试器的具体问题:您的解释是错误的。你看到的是函数调用的运行时堆栈,它让你执行时间线中的特定点,你现在正在。
不"未知",不某些事情"稍后会减少"。您已经通过它,到达当前的执行点。是什么,等待嵌套调用的结果,继续使用结果进行工作。
如果您再次点击 步骤 (使用p.37代码),您将会更深入地了解您的情况看到更多(rember)
出现在 堆栈区域中。您当前的执行点显示在 堆栈 的最顶层;最早 - 最底层。
请注意 变量 区域显示变量'该特定调用框架的值。
如果移动鼠标光标并将鼠标悬停在较低的(rember)
上并单击它,则会看到其变量'值:
Racket的调试器有点习惯了。
另请注意"最后评估值" 左上角中的字段以非常小的字母显示(在上一张图片中)。这是一个非常重要和有用的信息,同时调试。 可以使用 little 位在屏幕上更明显。
您没有看到(rember)
堆栈与第一个代码一起增长的原因(第34页),
是tail recursive。对于rember
的深层嵌套调用的结果,除了将其进一步返回之外没有任何事情要做;所以没有必要为此保存任何状态。这意味着rember
的调用框架被重用,替换,这就是为什么你只看到其中一个,在堆栈
但是使用p.37代码,返回值还需要做更多的事情 - 前面的列表元素必须cons
到结果上。这意味着必须保留列表元素,在某处记住。那个地方是rember
的调用框架,其中该列表元素作为(car lat)
被访问,的值为lat
,在那个指向执行时间表。
同样,对于具有(else (function (cdr ...
模式的所有其他函数,这意味着它们也是尾递归。但是,如果您看到(else (cons ... (function (cdr ...
之类的内容,那么它们不是。 cons
挡路了。
为了更好地了解发生了什么,我们用等式模式匹配伪代码重写它:
(rember34 a lat) =
{ (null? lat) -> '()
; (eq? a (car lat)) -> (cdr lat)
; else -> (rember a (cdr lat))
}
这进一步简化为三个条款,
rember34 a [] = []
rember34 a [a, ...as] = as
rember34 a [b, ...as] = rember a as
这个伪代码在视觉上是否可以理解,没有明确解释?我希望它是。另一个定义是
rember37 a [] = []
rember37 a [a, ...as] = as
rember37 a [b, ...as] = [b, ...rember a as]
现在只需看看这些定义,我们就可以看到差异,以及每个人的所作所为。
第一个rember34
沿着列表(这是它的第二个参数), (3rd clause)
直到找到{{1}在它(它的第一个参数)中,如果它是 a
,它将返回列表的其余部分 。如果找不到(2nd clause)
a
,我们已经到达列表的末尾 {{1 }} 以便继续的列表现在为空((3rd clause)
),返回空列表(1st clause)
[]
< / EM> 。
有道理。例如,
[]
第二个(1st clause)
执行相同但有一个关键区别:它将每个非匹配元素保留在它找到并删除之前(如前所述)。这意味着如果找不到这样的元素,将重新创建相同的列表。例如,
rember34 3 [1,2,3,4,5] % Tail-Recursive Call:
= rember34 3 [2,3,4,5] % Just Returning The Result...
= rember34 3 [3,4,5] % Call Frame Is Reused.
= [4,5]
rember34 3 [1,2]
= rember34 3 [2]
= rember34 3 []
= []
希望这能澄清事情。
旁注:在尾递归modulo cons
优化下,它是
rember37
非常喜欢它也在懒惰的评价之下!