【问题标题】:The Little Schemer evens-only*&coThe Little Schemer 仅偶数*&co
【发布时间】:2012-05-21 20:51:21
【问题描述】:

我很难理解第 145 页上 The Little Schemer 的 evens-only*&co 示例发生了什么。

代码如下:

(define evens-only*&co
 (lambda (l col)
   (cond
    ((null? l)
     (col '() 1 0))
    ((atom? (car l))
     (cond
      ((even? (car l))
       (evens-only*&co (cdr l)
                    (lambda (newl product sum)
                      (col (cons (car l) newl)
                           (opx (car l) product)
                           sum))))
      (else
       (evens-only*&co (cdr l)
                    (lambda (newl product sum)
                      (col newl product (op+ (car l) sum)))))))
    (else
     (evens-only*&co (car l)
                  (lambda (newl product sum)
                    (evens-only*&co (cdr l)
                                    (lambda (dnewl dproduct dsum)
                                      (col (cons newl dnewl)
                                           (opx product dproduct)
                                           (op+ sum dsum))))))))))

初始col可以是:

(define evens-results
 (lambda (newl product sum)
   (cons sum (cons product newl))))

我没有得到的是,l 作为'((1) 2 3),它立即进入最终的else(car l) 作为(1)(cdr l) 作为(2 3)。很好,但是我想从newlproductsum 中找出dnewldproductdsum,我的大脑一片空白。如果有人能指导我如何设置 DrRacket 或 Chez Scheme 或 MIT-Scheme 来运行步进器,那也会很有帮助。

但也许我说得太早了。第一次阅读这篇文章的初学者真的应该理解这种疯狂的延续吗?

【问题讨论】:

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


【解决方案1】:

我在第一次阅读时也发现这部分令人困惑,直到我在其他地方阅读了关于延续和延续传递风格(就是这样)之后才开始理解它。

冒着解释你已经得到的东西的风险,一种对我有帮助的看待它的方法是将“收集器”或“延续”视为替换函数返回值的正常方式。在正常的编程风格中,您调用一个函数,接收一个值,然后在调用者中对其进行处理。例如,标准递归 length 函数包括用于非空情况的表达式 (+ 1 (length (cdr list)))。这意味着一旦(length (cdr list)) 返回一个值,就会有一个计算等待它产生的任何值发生,我们可以将其视为(+ 1 [returned value])。在正常的编程中,解释器会跟踪这些待处理的计算,这些计算往往会“堆积”起来,正如您在本书的前几章中看到的那样。例如,在递归计算列表的长度时,我们有一个“等待计算”嵌套,其深度与列表的长度一样多。

在延续传递风格中,我们不是调用函数并在调用函数中使用返回的结果,而是通过为函数提供“延续”来告诉函数在产生其值时要做什么“打电话。 (这类似于您在异步 Javascript 编程中对回调所做的事情,例如:而不是编写 result = someFunction();,而是编写 someFunction(function (result) { ... }),并且所有使用 result 的代码都在回调函数中)。

这是length 的延续传递风格,仅供比较。我已经调用了延续参数return,它应该说明它在这里的工作方式,但请记住,它只是一个普通的Scheme 变量,就像其他任何变量一样。 (在这种风格中,延续参数通常称为k)。

(define (length/k lis return)
  (cond ((null? lis) (return 0))
        (else
         (length/k (cdr lis)
                   (lambda (cdr-len)
                     (return (+ cdr-len 1)))))))

an article on continuations by Little Schemer co-author Dan Friedman 中有一个阅读此类代码的有用提示。 (见第 8 页开始的第 II-5 节)。解释一下,上面的else 子句是这样说的:

假设您有在(cdr lis) 上调用length/k 的结果,并且 称它为cdr-len,然后加一个并传递这个加法的结果 继续 (return)。

请注意,这几乎正是解释器在函数的正常版本中评估 (+ 1 (length (cdr lis))) 时所要做的(除了它不必为中间结果 (length (cdr lis)) 命名。通过传递我们使控制流(和中间值的名称)显式化的延续或回调,而不是让解释器跟踪它。

让我们将此方法应用于evens-only*&co 中的每个子句。由于这个函数产生 三个 值而不是一个,所以这里稍微复杂了一点:删除了奇数的嵌套列表;偶数的乘积;和奇数的总和。这是第一个子句,其中(car l) 已知为偶数:

(evens-only*&co (cdr l)
                (lambda (newl product sum)
                  (col (cons (car l) newl)
                       (opx (car l) product)
                       sum)))

假设你有去除奇数的结果, 将偶数相乘,并从列表的cdr 中添加奇数, 并分别称他们为newlproductsumcons 列表的头部到newl(因为它是偶数,它应该去 结果);将product 乘以列表的头部(因为 我们正在计算偶数的乘积);离开sum;并通过这些 三个值给你等待继续col

以下是列表头部为奇数的情况:

(evens-only*&co (cdr l)
                (lambda (newl product sum)
                  (col newl product (op+ (car l) sum))))

和以前一样,但是将 newlproduct 的相同值传递给延续(即“返回”它们),以及 sum 和列表头部的总和,因为我们正在求和增加奇数。

这是最后一个,其中(car l) 是一个嵌套列表,由于双重递归而稍微复杂了一点:

(evens-only*&co (car l)
                (lambda (newl product sum)
                  (evens-only*&co (cdr l)
                                  (lambda (dnewl dproduct dsum)
                                    (col (cons newl dnewl)
                                         (opx product dproduct)
                                         (op+ sum dsum))))))

想象一下,你有从删除、求和和相加得到的结果 (car l) 中的号码并调用这些 newlproductsum;然后 想象一下你对(cdr l)做同样的事情的结果, 并称他们为dnewldproductdsum。给你的等待 继续,给出consing newldnewl 产生的值 (因为我们正在生成列表列表);相乘 productdproduct;并添加sumdsum

注意:每次我们进行递归调用时,我们都会为递归调用构造一个新的延续,它“关闭”参数的当前值 l 和返回延续 - col,在其他情况下换句话说,您可以将我们在递归期间建立的延续链视为对更传统编写函数的“调用堆栈”进行建模!

希望能部分回答您的问题。如果我有点过火了,那只是因为我认为,在递归本身之后,延续是 The Little Schemer 和一般编程中第二个非常简洁、扩展思维的想法。

【讨论】:

  • 谢谢 Jon O。我仍然不清楚第三种情况会发生什么,它是一个嵌套列表而不是原子。假设输入列表 l 是 ((2)),这意味着在第一次通过它时遇到案例 3:(2) 表示 (car l),() 表示 (cdr l),而我的偶数结果表示 col - 然后首先出现在递归中。重新输入 evens-only*&co,第一个条件为真,(null?l),dnewl,dproduct,dsum 取 '(), 1, 0。但这是第一次,大概 newl、product、sum 不会被初始化甚至不存在。但显然我看错了。 . . .
  • @melwasul ((2)) 转换为 col (cons (cons 2 ()) ()) (* (* 2 1) 1) (+ 0 0)
【解决方案2】:

Jon O.answer 对基本概念进行了非常深入的解释。虽然对我(希望对其他一些人也是如此)来说,当它们具有视觉表示时,对此类概念的理解会容易得多。

所以,我准备了两个流程图(类似于ones I did for multirember&co,解开evens-only*&co通话过程中发生的事情

给定l 是:

'((9 1 2 8) 3 10 ((9 9) 7 6) 2) 

col 是:

(define the-last-friend
    (lambda (newl product sum)
        (cons sum (cons product newl))
    )
)

一个流程图,反映变量在不同递归步骤中的关系 第二个流程图,显示正在传递的实际值

我希望这个答案将是对Jon's explanation above 的一个不错的补充。

【讨论】:

    【解决方案3】:

    我一直在阅读如何设计程序(felleisen 等人)。我正在浏览他们定义 local 定义的部分。我已经编写了一个代码,它使用本地定义实现了上述 evens-only&co。这是我写的:

    (define (evens-only&co l)
      (local ((define (processing-func sum prod evlst lst)
                (cond ((null? lst) (cons sum (cons prod evlst)))
                      ((atom? (car lst))
                       (cond ((even? (car lst)) (processing-func sum (* prod (car lst)) (append evlst (list (car lst))) (cdr lst)))
                             (else
                              (processing-func (+ sum (car lst)) prod evlst (cdr lst)))))
                      (else
                       (local ((define inner-lst (processing-func sum prod  '() (car lst))))
                       (processing-func (car inner-lst) (cadr inner-lst) (append evlst (list (cddr inner-lst))) (cdr lst)))))))
        (processing-func 0 1 '() l)))
    

    为了测试,当我输入 (evens-only&co '((9 1 2 8) 3 10 ((9 9) 7 6) 2)) 时,它返回 '(38 1920 (2 8) 10 (() 6 ) 2) 正如小计划者所期望的那样。但是,我的代码在一种情况下失败:当根本没有偶数时,偶数的乘积仍然显示为 1。例如 (evens-only&co '((9 1) 3 ((9 9) 7 )))返回'(38 1 () (()))。我想我需要一个额外的功能来纠正这个问题。 @melwasul:如果您不熟悉 local 定义,很抱歉在这里发布。我建议你也阅读 HTDP。这是一本适合初学者的好书。 但是方案专家也可以将他们的 cmets 发布在我的代码上。我对本地定义的理解正确吗?

    【讨论】:

    • 其实,如果列表中没有偶数,乘积应该是1是正确的:一个空的数字列表的乘积应该是乘法恒等式,1,只是因为空列表的总和是加法标识,0。例如,尝试在 Scheme 提示符处评估 (*)
    【解决方案4】:

    在等式伪代码中(类似KRC 的符号,为调用(f x y)f x y,它是明确的),这是

    evens-only*&co l col 
       = col [] 1 0                                     , IF null? l
       = evens-only*&co (cdr l) 
                        ( newl product sum =>
                            col (cons (car l) newl)
                                (opx (car l) product)
                                sum )                   , IF atom? (car l) && even? (car l)
       = evens-only*&co (cdr l) 
                        ( newl product sum =>
                            col  newl  product  (op+ (car l) sum) )      , IF atom? (car l)
       = evens-only*&co (car l) 
                        ( anewl aproduct asum =>
                             evens-only*&co (cdr l)
                                            ( dnewl dproduct dsum =>
                                                 col (cons anewl    dnewl)
                                                     (opx  aproduct dproduct)
                                                     (op+  asum     dsum) ) )   , OTHERWISE
    

    这是一个CPS 代码,它从输入嵌套列表(即树)中收集所有偶数,同时保留树结构,并找到所有偶数的乘积;至于非偶数,它总结它们:

    • 如果l 是一个空列表,则三个基本(身份)值作为参数传递给col;

    • 如果(car l)是偶数,处理(cdr l)的结果是newlproductsum然后它们作为参数传递到col,而前两个通过consing ⁄ 乘以(car l)(偶数)来增加;

    • 如果(car l)是一个不是偶数的原子,则处理(cdr l)的结果是newlproductsum然后它们作为参数传递给col,第三个通过与(car l)(非偶数原子)求和而增加;

    • 如果(car l)是一个列表,则(car l)的处理结果为anewlaproductasum然后处理@的结果987654348@ 是dnewldproductdsum然后三个组合结果作为参数传递给col

    基本情况的[]10 分别是列表、乘法下的数字和加法下的数字的monoids 的标识元素。这只是意味着组合到结果中时不会改变结果的特殊值。

    作为说明,对于'((5) 2 3 4)(与问题中的示例接近),它会创建计算

    evens-only*&co [[5], 2, 3, 4] col
    =
    col  (cons []                   ; original structure w only the evens kept in,
               (cons 2              ;   for the car and the cdr parts
                  (cons 4 [])))
         (opx 1                     ; multiply the products of evens in the car and 
              (opx 2 (opx 4 1)))    ;   in the cdr parts
         (op+ (op+ 5 0)             ; sum, for the non-evens
              (op+ 3 0))     
    

    类似于my other answer(对于姐妹问题),这是另一种写法,使用模式匹配伪代码(带警卫):

    evens-only*&co  =  g   where
      g [a, ...xs...] col 
             | pair? a    = g a  ( la pa sa =>
                             g xs ( ld pd sd =>
                                            col [la, ...ld...] (* pa pd) (+ sa sd) ) )
             | even? a    = g xs ( l p s => col [ a, ...l... ] (* a  p )       s     )
             | otherwise  = g xs ( l p s => col         l            p   (+ a  s )   )
      g []            col =                 col []              1         0
    

    这种表示法的经济性(和多样性)确实让这一切变得更清晰、更容易只是看到,而不是迷失在函数和变量之类的长名称、括号重载的单词沙拉中作为列表数据、子句分组(如cond 表达式)、名称绑定(lambda 表达式)和函数调用指示符的语法分隔符,所有看起来完全相似。 S 表达式符号的相同统一性非常有利于机器操作(即 lisp 的 read 和宏),这不利于它的 人类 可读性.

    【讨论】:

      猜你喜欢
      • 2012-09-06
      • 2011-11-10
      • 2012-05-16
      • 2015-10-22
      • 2014-06-28
      • 2011-10-23
      • 2010-12-15
      • 2018-04-12
      • 2010-12-30
      相关资源
      最近更新 更多