[Little Schemer Ch3 pp.34& 37]:为什么(rember a(cdr lat))作为第37个示例的第2个参数被解释为未知的p.37示例

时间:2018-03-11 02:12:02

标签: debugging recursion racket tail-recursion the-little-schemer

我使用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被解释的原因。

谢谢!

2 个答案:

答案 0 :(得分:2)

让我强烈建议你在这里使用步进器,而不是调试器。我想你会看到一套更加一致的减少规则。具体来说,我认为你不会看到任何“被识别为未知的东西。”

要使用步进器:打开一个新的缓冲区,确保语言级别设置为带有列表缩写的开始学生,并将定义和调用粘贴到定义窗口中。点击“步骤”。我想你会很快看到两个评估之间的差异。

如果没有任何意义,请询问后续问题!

答案 1 :(得分:2)

TL; DR:你看到(和误解)的是函数调用的堆栈,以及尾递归的影响。 / p>

回答有关调试器的具体问题:您的解释是错误的。你看到的是函数调用的运行时堆栈,它让你执行时间线中的特定点,你现在正在

"未知",某些事情"稍后会减少"。您已经通过它,到达当前的执行点。是什么,等待嵌套调用的结果,继续使用结果进行工作。

如果您再次点击 步骤 (使用p.37代码),您将会更深入地了解您的情况看到更多(rember)出现在 堆栈区域中。您当前的执行点显示在 堆栈 的最顶层;最早 - 最底层。

enter image description here

请注意 变量 区域显示变量'该特定调用框架的值。

如果移动鼠标光标并将鼠标悬停在较低的(rember)上并单击它,则会看到变量'值:

enter image description here

Racket的调试器有点习惯了。

另请注意"最后评估值" 左上角中的字段以非常小的字母显示(在上一张图片中)。这是一个非常重要和有用的信息,同时调试。 可以使用 little 位在屏幕上更明显。

您没有看到(rember)堆栈与第一个代码一起增长的原因(第34页),

enter image description here

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

非常喜欢它也在懒惰的评价之下!