【问题标题】:[Little Schemer Ch3 pp.34 & 37]: Why (rember a (cdr lat)) as the 2nd argument of cons interpreted as unknown on p.37 example[Little Schemer Ch3 pp.34 & 37]:为什么 (rember a (cdr lat)) 作为 cons 的第二个参数在 p.37 示例中解释为未知
【发布时间】:2018-03-11 02:12:02
【问题描述】:

我使用 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 ...)
(记住……)

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 ...)
(记住……)
(记住……)

带有p.37代码的Stack区域表示rember的第二次调用在处理(cdr lat)之前已经被归类为unknown

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

没有(cdr lat)rember 本身不会返回列表。并且本书前 40 页中所有包含(cdr lat) 的示例都具有相同的(function (cdr variable) 格式。

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

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

谢谢!

【问题讨论】:

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


    【解决方案1】:

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


    回答您关于调试器的具体问题:您的解释是错误的。您所看到的是函数调用的运行时堆栈,这些调用将您带到执行时间线中您现在所处的特定点

    不是“未知”,不是“以后会减少”的东西。您已经通过它,到达您当前的执行点。它是什么,正在等待嵌套调用的结果,以继续处理 结果

    如果您再点击 Step 几次(使用 p.37 代码),您将进入更深的点,您会看到更多 @ 987654329@s 出现在堆栈区域。您当前的执行点出现在 堆栈 的最顶端;最早的——最底层的。

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

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

    Racket 的调试器有点习惯了。

    还要注意左上角中以非常小的字母显示的“最后评估值”字段(在上图中)。这是调试时非常重要且有用的信息。它可以在屏幕上显示得一点

    您没有看到(rember)s 的堆栈随着第一个代码(p.34)而增长的原因,

    tail recursive。对rember 的深层嵌套调用的结果没有什么可做的,只是将其返回更远;所以没有必要为此保存任何状态。这意味着rember调用框架会被重用、替换,这就是为什么您只能在堆栈的底部看到其中一个。

    但是对于 p.37 的代码,需要对返回值进行更多处理 - 前面的列表元素必须 consed 到结果中。这意味着必须保留列表元素,并在某处记住它。某处是rember 的调用框架,其中列表元素作为(car lat) 访问,对于thatlat,在执行时间线中的that 点。

    同样,对于具有(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) 直到在其中找到 a(它的第一个参数),如果它执行 (2nd clause),它会返回列表的其余部分此时。如果没有找到a (3rd clause) 并且我们已经到达列表的末尾(1st clause) sup> 以使要继续的列表现在为空 ([]),将返回空列表 [](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,做同样的事情,但有一个关键的区别:它将每个不匹配的元素保留在它找到并删除的元素之前(和以前一样)。这意味着如果没有找到这样的元素,将重新创建相同的列表。例如,

    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
    

    希望这可以澄清事情。


    旁注:在尾递归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]
    

    这很像它也会处于惰性评估之下!

    【讨论】:

    • 谢谢!现在对 p37 有了自己的解释。如果我是正确的,请告诉我。除了调用rember变量外,还有2组流程——“Before else”和“else”。“else”部分只能在“before else”部分得到结果后执行。这部分不涉及第二次调用变量变量调用部分在 p34 和 p37 示例上是相同的,只是传递“before else”过程的值。两个示例之间的唯一区别是 p34 的 else 部分过程只是第二次调用变量。虽然有一个p37 示例的 else 部分的独立进程
    • 只有一个进程。通常,当我们达到(cons (car lat) (rember a (cdr lat))) 时,我们需要评估(cons A B) 表单。 A 的值很容易找到。求B的值,我们求C的值=(cdr lat)(也很简单找),去旁边找B的值=(rember C)同理规则。现在我们已经计算了B,我们继续我们的R = (cons A B) 任务。既然我们有AB,那么计算R 也很容易。
    • 我明白你所说的,非常感谢。我的意思是数据流的观点。 lat 的值只是在 null 之间循环吗? > 情商? > Cdr lat(记忆)。第二个进程 (cons...) 是有条件的,并且只会在第一个进程(如果 eq? = #t)得到除 null 以外的结果后执行。 TLS 是我学习编程的第一步。这确实是一个挑战,但我知道我正在逐页取得进展!再次感谢
    • 啊,好的。然后不是“循环”,而是在cond 的两个子句之间“切换”;甚至“通过”。这就是cond 的工作原理,该过程按顺序依次执行其子句,直到条件返回 true 值的那个 - 然后,输入 this 子句。碰巧它包含一个递归调用,这导致我们的进程再次遍历这两个子句,再次尝试它们的条件 - 但在一个新的、更深级别的递归.不客气,一路走好!
    • 不是“循环”然后是“切换”,如您所见,像我这样的初学者通常甚至不能正确地提出问题。 (atom?) 的标准代码是 (and (not (pair? atom)) (not (null? atom)))。而不是真假。是否可以重写此函数并给出结果 '(not atom but a list) OR '(not atom but an empty list) OR '(this is an atom)?
    【解决方案2】:

    让我强烈建议您在这里使用步进器,而不是调试器。我想你会看到一套更一致的缩减规则。具体来说,我认为您不会看到任何“被确定为未知”的内容。

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

    如果有什么不明白的地方请追问!

    【讨论】:

    • OP 误解了堆栈显示。差异是由于尾/非尾递归。 (我已经更新了我的答案,即使有关于 TR 的图片和俳句……此时没人会看到它;哦,好吧)。
    • 我尝试了初级和中级,都很棒。但是,那些学习版本的 Racket 似乎缺少一些原语。至少我注意到(对?)没有定义OOTB。所以我使用 (list? ) 来定义 (atom? )
    • 您应该可以使用“缺点”?代替“对?”。还有其他人吗?
    • Racket 文档应该有这个列表。我很早就看到了。不知道有什么坏处?今晚会试试!谢谢
    猜你喜欢
    • 2022-01-15
    • 2012-09-06
    • 2011-10-23
    • 1970-01-01
    • 2021-12-25
    • 2012-12-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多