【问题标题】:Explain the continuation example on p.137 of The Little Schemer解释 The Little Schemer 的 p.137 的延续例子
【发布时间】:2011-10-23 16:40:51
【问题描述】:

有问题的代码是这样的:

(define multirember&co
  (lambda (a lat col)
    (cond
     ((null? lat)
      (col (quote ()) (quote ())))
     ((eq? (car lat) a)
      (multirember&co a
                      (cdr lat)
                      (lambda (newlat seen)
                        (col newlat
                             (cons (car lat) seen)))))
     (else
      (multirember&co a
                      (cdr lat)
                      (lambda (newlat seen)
                        (col (cons (car lat) newlat)
                             seen))))))

我已经盯着这个看了一整天,但我似乎不太明白。当您重新定义函数时,您正在重新定义col,但在示例中它们似乎使用了原始定义。为什么不改。如果不传入参数newlatseen,你怎么能重复它。

很难解释我的问题,因为我似乎只是漏掉了一部分。如果也许有人可以提供比这本书更明确的演练,我也许能够理解它是如何工作的。

【问题讨论】:

  • 您的代码缺少一个右括号。

标签: recursion lambda scheme continuation-passing the-little-schemer


【解决方案1】:

让我们通过一个例子来说明;也许这会有所帮助。 :-) 为简单起见,我将使用list 作为收集器/延续,它只会返回一个包含延续参数的列表。

(multirember&co 'foo '(foo bar) list)

一开始,

a = 'foo
lat = '(foo bar)
col = list

在第一次迭代中,(eq? (car lat) a) 条件匹配,因为lat 不为空,并且lat 的第一个元素是'foo。这样就设置了到multirember&co 的下一个递归:

a = 'foo
lat = '(bar)
col = (lambda (newlat seen)
        (list newlat (cons 'foo seen))

在下一次迭代中,else 匹配:因为lat 不为空,并且lat 的第一个元素是'bar(而不是'foo)。因此,对于下一次递归,我们有:

a = 'foo
lat = '()
col = (lambda (newlat seen)
        ((lambda (newlat seen)
           (list newlat (cons 'foo seen)))
         (cons 'bar newlat)
         seen))

为了便于人类阅读(并避免混淆),我们可以重命名参数(由于词法作用域),而不改变程序的语义:

col = (lambda (newlat1 seen1)
        ((lambda (newlat2 seen2)
           (list newlat2 (cons 'foo seen2)))
         (cons 'bar newlat1)
         seen1))

最后,(null? lat) 子句匹配,因为 lat 现在为空。所以我们叫

(col '() '())

扩展为:

((lambda (newlat1 seen1)
   ((lambda (newlat2 seen2)
      (list newlat2 (cons 'foo seen2)))
    (cons 'bar newlat1)
    seen1))
 '() '())

其中(替换 newlat1 = '()seen1 = '() 时)变为

((lambda (newlat2 seen2)
   (list newlat2 (cons 'foo seen2)))
 (cons 'bar '())
 '())

或(评估(cons 'bar '())

((lambda (newlat2 seen2)
   (list newlat2 (cons 'foo seen2)))
 '(bar)
 '())

现在,替换值 newlat2 = '(bar)seen2 = '(),我们得到

(list '(bar) (cons 'foo '()))

或者,换句话说,

(list '(bar) '(foo))

给出我们的最终结果

'((bar) (foo))

【讨论】:

  • 首先,感谢您详尽周到的解释。对于如何获得内部 lambda 的实际参数,我仍然有点模糊。你怎么知道 newlat2 = '(bar) 和 seen2 = '()?
  • 让我们通过一个更简单的示例来说明参数替换的工作原理。 :-) 假设你有一个函数(define ** (lambda (x y) (exp (* (log x) y))))。如果您随后调用(** 42 24),那么将调用带有x = 42y = 24 的lambda。由于** 与lambda 相同,因此等效表达式为((lambda (x y) (exp (* (log x) y))) 42 24)。希望这有点道理。 :-)
  • 现在直接回答您关于我们如何获得newlat2 = '(bar)seen2 = '() 的问题,它只是用给定的参数调用那个lambda。考虑表达式((lambda (newlat2 seen2) ...) '(bar) '())。如果我们给这个 lambda 起一个名字,比如inner,那么这个表达式就变成了(inner '(bar) '())。那么,很容易看出为什么在 lambda 内部,newlat2seen2 会列出这些值。
  • 哦,哇,好吧,我想我现在看到了。你用你最后的解释把它钉牢了。我现在明白为什么 lambda 在它们之前有两个括号了。第一个是定义函数,第二个是评估它,最后包含两个参数。非常感谢!
【解决方案2】:

我在这里找到了一个绝妙的答案: http://www.michaelharrison.ws/weblog/?p=34

我也一直在努力解决这个问题。关键是理解词法作用域(对我来说,à la Javascript)和传递给 eq 而不是 eq 分支上的 multirember&co 的内部函数。明白这一点,你就会明白整个过程。

【讨论】:

    【解决方案3】:

    上面的链接 (http://www.michaelharrison.ws/weblog/?p=34) 很好地解释了整个练习如何避免命令式编程(C、Java)需要声明两个“持有者”或“收集器”变量(或列表、向量)在内存中显式地在您遍历列表时捕获您的答案。使用 FP 语言 Scheme 的 continuation,当您逐步(草莓金枪鱼和箭鱼)进入任何单独创建的“篮子”时,您不会“推”测试结果;相反,您在发送适当的 consing 函数时将两个列表组合在一起 - 一个用于 eq?是的,另一个为 eq? false - 通过递归。 . .最后以第三个 col 函数结束,在 TLS 的第一个示例中,它是“a-friend”,它询问为保存所有匹配而构建的列表是否为空(null?)。然后,TLS 要求您再次“运行”multirember&co,使用一个新的“last”col,它只询问包含所有“not tuna”原子的列表它包含多少(“last-friend”)。因此,有两个“第一类”函数用于处理收集任务,即构建两个单独的列表,然后在递归展开结束时,原始 col(“a-friend”)提出最后一个问题。也许“multirember&co”这个名字不是最伟大的名字,因为它确实没有重建列表减去要删除的原子;相反,它构建了两个单独的列表——它们永远不会被显示——然后应用最后的 col (a-friend or last-friend) 。 . .它显示#t 或#f,或“非金枪鱼”列表的长度。

    这是一些输出:

    > (multirember&co 'tuna '(and tuna) a-friend)
    #f
    > (multirember&co 'tuna '(and not) a-friend)
    #t
    

    这是一个返回不匹配列表的col:

    (define list-not  (lambda (x y) x))
    

    及其用途:

    > (multirember&co 'tuna '(and not) list-not)
    (and not)
    

    【讨论】:

      【解决方案4】:

      希望这篇攻略对你有帮助

      正如 Chris 所建议的,我已将 newlat/seen 重命名为 n/s 并添加了一个索引。 这本书给函数起了可怕的名字(a-friend new-friend latest-fried),所以我只保留了 L(代表 lambda)和定义。

      multirember&co 'tuna '(strawberries tuna and swordfish) a-friend)
        multirember&co 'tuna '(tuna and swordfish) (L(n1 s1)(a-friend (cons 'strawberries n1) s1))
          multirember&co 'tuna '(and swordfish) (L(n2 s2)((L(n1 s1)(a-friend (cons 'strawberries n1) s1)) n2 (cons 'tuna s2))
            multirember&co 'tuna '(swordfish) (L(n3 s3)((L(n2 s2)((L(n1 s1)(a-friend (cons 'strawberries n1) s1)) n2 (cons 'tuna s2)) (cons 'and n3) s3))
              multirember&co 'tuna '() (L(n4 s4)((L(n3 s3)((L(n2 s2)((L(n1 s1)(a-friend (cons 'strawberries n1) s1)) n2 (cons 'tuna s2)) (cons 'and n3) s3)) (cons 'swordfish n4) s4))
      
      ((lambda(n4 s4)((lambda(n3 s3)((lambda(n2 s2)((lambda(n1 s1)(a-friend (cons 'strawberries n1) s1)) n2 (cons 'tuna s2))) (cons 'and n3) s3)) (cons 'swordfish n4) s4)) '() '())
                     ((lambda(n3 s3)((lambda(n2 s2)((lambda(n1 s1)(a-friend (cons 'strawberries n1) s1)) n2 (cons 'tuna s2))) (cons 'and n3) s3)) '(swordfish) '())
                                    ((lambda(n2 s2)((lambda(n1 s1)(a-friend (cons 'strawberries n1) s1)) n2 (cons 'tuna s2))) '(and swordfish) '())
                                                   ((lambda(n1 s1)(a-friend (cons 'strawberries n1) s1)) '(and swordfish) '(tuna))
                                                                  (a-friend '(strawberries and swordfish) '(tuna))
      

      【讨论】:

        【解决方案5】:

        代码不会像通常那样构建解决方案,但它会构建一个计算解决方案的代码,就像您使用低级操作构建树时一样,例如cons+、@987654323 @等,而不是使用高级累加器或过滤器。

        这就是为什么很难说过程是迭代还是递归的原因,因为根据迭代过程的定义,它们为本地状态使用有限的内存。但是这种进程占用的内存很大,而且是在环境中分配的,而不是在本地参数中分配的。

        首先,我在这里复制代码,以便能够在不滚动太多的情况下看到对应关系:

        (define multirember&co
          (lambda (a lat col)
            (cond
             ((null? lat)
              (col (quote ()) (quote ())))
             ((eq? (car lat) a)
              (multirember&co a
                              (cdr lat)
                              (lambda (newlat seen)
                                (col newlat
                                     (cons (car lat) seen)))))
             (else
              (multirember&co a
                              (cdr lat)
                              (lambda (newlat seen)
                                (col (cons (car lat) newlat)
                                     seen)))))))
        

        让我们尝试拆分问题,看看到底发生了什么。

        • 案例一:

        (multirember&co 'a
                        '()
                        (lambda (x y) (list x y)))
        
        is the same as    
        
        (let ((col (lambda (x y) (list x y))))
          (col '() '()))
        

        这是一个小例子,它永远不会循环。

        现在有趣的案例:

        • 案例 2:

        (multirember&co 'a
                        '(x)
                        (lambda (x y) (list x y)))
        
        is the same as    
        
        (let ((col
               (let ((col (lambda (x y) (list x y)))
                     (lat '(x))
                     (a 'a))
                 (lambda (newlat seen)
                   (col (cons (car lat) newlat)
                        seen)))))
          (col '() '()))
        

        在这种情况下,进程会生成此代码作为结果,并最终对其进行评估。请注意,在本地它仍然是尾递归的,但在全局上它是一个递归过程,它需要内存不是通过分配某些数据结构,而是让评估器仅分配环境帧。每个循环通过添加 1 个新帧来加深环境。

        • 案例3

        (multirember&co 'a
                        '(a)
                        (lambda (x y) (list x y)))
        
        is the same as    
        
        (let ((col
               (let ((col (lambda (x y) (list x y)))
                     (lat '(a))
                     (a 'a))
                 (lambda (newlat seen)
                   (col newlat
                        (cons (car lat) seen))))))
          (col '() '()))
        

        这会构建代码,但在另一个分支上,它将结果累积到另一个变量中。


        所有其他情况都是这3种情况中的一种的组合,通过添加一个新层,每个1的作用都很清楚。

        【讨论】:

          【解决方案6】:

          很长一段时间以来,我一直在努力理解multirember&co 内部发生的事情。问题是,当我认为我已经得到它的那一刻 - 下一个任务/示例证明我没有。

          帮助我将正在发生的事情的可视化表示放在一起(对我来说,文本演练很难掌握,出于某种原因)。

          所以,我整理了两个流程图:

          一,只显示递归不同步骤之间的关系



          还有一个,反映实际值



          现在,每当我觉得我再次失去了“争论的线索”时,我只需参考这个流程图,它就会让我回到正轨。

          通过流程图查看“全图”后,我了解到的另一件事是,a-friend 函数只是检查seen 是否包含任何值(尽管它以相反的方式返回它,即#fseen#t 中有值时seen 为空,这可能会造成混淆。

          P.S.:我写了similar flow-charts for evens-only*&co,这本书后面会出现。

          【讨论】:

          • 这些图表很棒(显然我在 2017 年 11 月 12 日之前的某个时间点赞了 :));很好地展示了在从基本案例返回的路上构建的 两个 值的管道。不过,这里和那里都缺少一些括号,我认为你已经在不同的地方切换了两个图表:你说的是“反映实际值”的那个显示了变量名; 另一个使用这些值。总体而言,可视化效果极佳。
          • @WillNess,感谢您通知图像纠缠(已修复)。稍后我也会看看遗漏的括号。
          • 缺少括号现在已修复(我相信 =)
          • @WillNess,我很感谢你看一下(很犹豫明确要求你这样做)
          【解决方案7】:

          为了清楚起见,让我们使用一些省略括号的等式伪代码(因此,我们为调用 (f x y) 编写 f x y,这是明确的):

          multirember&Co a lat col
              = col [] []                                , IF lat == [] 
          
              = multirember&Co a (cdr lat)
                   ( newlat seen => 
                       col newlat
                           (cons (car lat) seen) )       , IF (car lat) == a
          
              = multirember&Co a (cdr lat)
                   ( newlat seen => 
                       col (cons (car lat) newlat)
                           seen )                        , OTHERWISE
          

          这不就是不言自明的,这是做什么的? :) 还没有? :) 用想象中的模式匹配伪代码(带警卫)再次重写,我们有

          multirember&Co  =  g   where
              g a [b, ...lat] col | b == a  =  g a lat ( n s =>  col     n     [b, ...s] )
                                  | else    =  g a lat ( n s =>  col [b, ...n]     s     )
              g a []          col           =                    col     []        []
          

          模式匹配的语义应该很明显:[b, ...lat] 匹配 [1,2,3] 其中b = 1lat = [2,3]。因此,这只是一个三种情况的方程:

          • 当第二个参数是一个空列表时,“收集器”函数col 立即被输入两个空列表作为它的两个参数;

          • 当第二个参数的head元素与第一个参数相同时,结果与tail递归相同 em> 列表中,修改后的收集器 -- 之后 它将接收它的两个参数,ns, -- 前置当前头部元素(即a)添加到s 列表中,并将这两个列表提供给this 调用的收集器函数col

          • 1234563 /p>

          换句话说,我们正在处理从递归调用返回的两个结果,如果头部是 a,则将头部添加到第二个结果,或者如果不是,则将头部添加到第一个。

          因此调用

              (g 1 [ 2, 1, 3, 1, 4, 5 ] col)
          

          与调用相同(将导致)

              (col [ 2, ...[3, ...[4, ...[5, ...[]]]]]
                   [    1, ...[1,            ...[]]  ])
          

              (col [ 2,     3,     4,     5          ]
                   [    1,     1                     ])
          

          另一种看待它的方式是,以下是另一种等效的表述:

          multirember&Co a lat col  =  g a lat id id   where
              id      x  =  x              ; identity function  
              (f ∘ g) x  =  f (g x)        ; function composition
              g a [b, ...lat] c d 
                         | b == a  =  g a lat  c     (d ∘ (x => cons b x))  ;    (d ∘ {cons b})
                         | else    =  g a lat (c ∘ (x => cons b x))   d     ; (c ∘ {cons b})
              g a []          c d  =  col     (c [])                (d [])
          

          因此

          multirember&Co 1 [ 2, 1, 3, 1, 4, 5 ] col 
          =
          col (((((id ∘ {cons 2}) ∘ {cons 3}) ∘ {cons 4}) ∘ {cons 5}) [])   ; { } is for
              ( ( (id ∘       {cons 1}) ∘ {cons 1}                  ) [])   ;  partial application
          =
          col     (id   (cons 2     (cons 3     (cons 4    (cons 5   [])))))
                  (id         (cons 1     (cons 1                    []) ) )  
          

          这显然是一回事。

          在另一个伪代码(带有列表推导)中,这表明它自己是

          multirember&Co a lat col  
             = col [ b for b in lat if (b /= a) ] 
                   [ b for b in lat if (b == a) ] 
             = ( ((n,s) => col n s) ∘ {partition {/= a}} ) lat
          

          除了列表lat一次 遍历(在原始代码中),有效地构建模仿原始列表结构的嵌套 lambda 函数链;然后评估哪个链来创建两个结果,将它们传递给最顶层的收集器函数col

          所有这一切都向我们展示了Continuation-Passing Style(就是这样)的强大功能,它实际上创建了自己的函数调用协议,例如从每个递归函数传回两个结果调用,即使通常在 lambda 演算中,一个函数只能有 一个 结果(即使是一对)。

          【讨论】:

          • 好吧,我很荣幸,当然,你征求我的意见 =)。老实说:直到最后一个代码 sn-p(一个带有列表理解的代码)——我认为虽然这个答案解释/说明了发生了什么,但我很难评估它,因为我已经掌握了它已经解释的大部分内容。 虽然,您的列表理解示例“挽救了一天”(它让我从另一个角度看到了这种编码风格)。因此,感谢您在此答案中投入的时间和精力!
          • 我的意思是在视觉上,它是否有助于轻松看到正在发生的事情。对我来说,从上数第二个是最清楚的。尽管我知道它做了什么,原始 Scheme代码对我来说就像一堵文字墙。当我阅读它时,我一次最多只能看到一个子句,所以我不得不记住其余的。不过,使用该伪代码,我可以一次看到所有内容。当然是 YMMV。感谢您的反馈!
          猜你喜欢
          • 1970-01-01
          • 2010-12-15
          • 2010-12-30
          • 1970-01-01
          • 2012-05-16
          • 2014-06-28
          • 2012-09-06
          • 2015-10-22
          • 2011-06-14
          相关资源
          最近更新 更多