[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)
可以已处理。
或者为什么 rember
在 cons
的第二个参数的位置被这样解释。
谢谢!
让我强烈建议您在这里使用步进器,而不是调试器。我想你会看到一套更一致的减少规则。具体来说,我认为您不会看到任何东西 "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 recursive。 rember
的深层嵌套调用的结果没有什么可做的,除了 return 它更靠后;所以没有必要为此保存任何状态。这意味着 rember
的 调用框架 被重用、替换,这就是为什么你只看到其中一个,在 堆栈 [= 的底部136=].
但是对于 p.37 的代码,returned 值还有更多的事情要做——前面的列表元素必须 cons
ed 到结果中。这意味着必须保留列表元素,记住某处。某处是 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]
这很像它也在惰性评估下!
我 运行 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)
可以已处理。
或者为什么 rember
在 cons
的第二个参数的位置被这样解释。
谢谢!
让我强烈建议您在这里使用步进器,而不是调试器。我想你会看到一套更一致的减少规则。具体来说,我认为您不会看到任何东西 "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 recursive。 rember
的深层嵌套调用的结果没有什么可做的,除了 return 它更靠后;所以没有必要为此保存任何状态。这意味着 rember
的 调用框架 被重用、替换,这就是为什么你只看到其中一个,在 堆栈 [= 的底部136=].
但是对于 p.37 的代码,returned 值还有更多的事情要做——前面的列表元素必须 cons
ed 到结果中。这意味着必须保留列表元素,记住某处。某处是 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]
这很像它也在惰性评估下!