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

[Little Schemer Ch3 pp.34 & 37]: Why (rember a (cdr lat)) as the 2nd argument of cons interpreted as unknown on p.37 example

我 运行 p.34 和 p.37 中的示例都使用 DrRacket 调试模式逐步进行。以下是两个示例第一次处理 (cdr lat) 时的堆栈 windows 结果。

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 …)

p.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 …)

Stack area with p.37's code表示在处理(cdr lat).[=28之前,rember的第二次调用已被分​​类为unknown =]

2个例子的唯一区别是第37页添加了“cons”。 Cons 接受 2 个参数,一个 s 表达式和一个列表。

没有 (cdr lat)rember 本身就没有 return 列表。本书前 40 页内所有包含 (cdr lat) 的示例都具有相同的 (function (cdr variable) 格式。

我不明白为什么 p.37 示例 rember 本身被识别为 unknown 并且有理由等待减少,而包含的 (cdr lat) 可以已处理。

或者为什么 rembercons 的第二个参数的位置被这样解释。

谢谢!

让我强烈建议您在这里使用步进器,而不是调试器。我想你会看到一套更一致的减少规则。具体来说,我认为您不会看到任何东西 "identified as unknown."

要使用步进器:打开一个新缓冲区,确保语言级别设置为 beginning student with list abbreviations,并将定义和调用粘贴到定义中 window。点击 "step"。我想您会很快看出这两个评估之间的区别。

如果有任何不明白的地方,请提出后续问题!

TL;DR: 您所看到的(和误解的)是 函数调用堆栈 ,以及 尾递归.


回答你关于调试器的具体问题:你的解释是错误的。您看到的是函数调用的 run-time 堆栈 ,它让您到达 执行时间轴中您现在所在的特定点.

不是"unknown",不是"to be reduced later"。你已经通过它,到达你当前的执行点。它是什么,正在等待嵌套调用的结果,以继续使用 结果 .

进行工作

如果你再点击Step几次(p.37代码),你会进入更深层次在 Stack 区域 中,您会看到更多 (rember)。您当前的执行点出现在Stack上top-most;最早的 – bottom-most.

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

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

Racket 的调试器有点习惯了。

还要注意 左上角 非常小的字母 中的 "last evaluated value" 字段(在上图中).在调试时,这是一条非常重要且有用的信息。 可以使用一点在屏幕上更明显。

您看不到 (rember) 的堆栈随着第一个代码(第 34 页)增长的原因,

就是tail recursiverember 的深层嵌套调用的结果没有什么可做的,除了 return 它更靠后;所以没有必要为此保存任何状态。这意味着 rember 调用框架 被重用、替换,这就是为什么你只看到其中一个,在 堆栈 [= 的底部136=].

但是对于 p.37 的代码,returned 值还有更多的事情要做——前面的列表元素必须 consed 到结果中。这意味着必须保留列表元素,记住某处。某处是 rember 的调用框架,其中该列表元素作为 (car lat) 访问,对于 lat 值,在 那个执行时间线中的点。

同样,对于所有其他具有 (else (function (cdr ... 模式的函数,这意味着它们也是 tail-recursive。但是,如果您看到类似 (else (cons ... (function (cdr ... 的内容,则它们是 而不是 cons 挡路了。


为了更好地了解发生了什么,我们用等式 pattern-matching 伪代码重写它:

(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) 直到它在其中找到 a(它的第一个参数),如果找到 (2nd clause),它 return在那个点是列表的其余部分。如果没有找到 a (3rd clause) 并且我们已经到了列表的末尾 (1st clause) 这样要继续的列表现在是空的 ([]),空列表 [] 是 returned (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 []
 = []

第二个 rember37 做同样的事情,但有一个关键的区别:它将每个 non-matching 元素保留在它找到和删除的元素之前(和以前一样)。这意味着如果没有找到这样的元素,将重新创建相同的列表。例如,

rember37 3 [1,2,3,4,5] 
 = [1, ...rember37 3 [2,3,4,5]]              % the->
       => [2, ...rember37 3 [3,4,5]]         %     stack->
              <= [4,5]                       %           grows
       <= [2,4,5]                            %      <-and
 = [1,2,4,5]                                 %  <-contracts

rember37 3 [1,2] 
 = [1, ...rember37 3 [2]]                    % must remember 1,
       => [2, ...rember37 3 []]              %     and 2, to be used
              <= []                          %          after the recursive call
       <= [2]                                %      has returned its result
 = [1,2]                                     %  to its caller

希望这能澄清事情。


旁注:在 tail-recursion modulo cons 优化下,它会是

rember37 3 [1,2,3,4,5] 
 = [1, ...rember37 3 [2,3,4,5]]
 = [1, ...[2, ...rember37 3 [3,4,5]]]
 = [1,2, ...rember37 3 [3,4,5]]
 = [1,2, ...[4,5]]
 = [1,2,4,5]

rember37 3 [1,2] 
 = [1, ...rember37 3 [2]]
 = [1, ...[2, ...rember37 3 []]]
 = [1,2, ...rember37 3 []]
 = [1,2, ...[]]
 = [1,2]

这很像它也在惰性评估下!