解释The Little Schemer第137页的续例

时间:2011-08-10 00:29:17

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

有问题的代码是:

(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的情况下重复使用。

很难解释我的问题,因为我似乎只是错过了一块。如果有人能够比书更明确地进行演练,我可能会理解它是如何运作的。

7 个答案:

答案 0 :(得分:21)

让我们来看一个例子;也许这会有所帮助。 :-)为简单起见,我只是使用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))

答案 1 :(得分:7)

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

我也一直在努力解决这个问题。关键是要理解词法范围(对我来说,javascript)和内部函数传递给eq上的multirember& co而不是eq分支。了解这一点,您将了解整个过程。

答案 2 :(得分:3)

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

这是一些输出:

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

答案 3 :(得分:2)

我已经努力了解自己,了解multirember&co内部发生的事情已有一段时间了。问题在于我认为我得到它的那一刻 - 下一个任务/例子证明我没有。

有什么帮助我整理了一个关于正在发生的事情的视觉表现(对于我来说,文字演练很难掌握,出于某种原因)。

所以,我把两个流程图放在一起:

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

Visual walkthrough showing relations

另一个,反映实际值

Visual walkthrough with values

现在,每当我觉得我失去了一个争论的主线。再说一次,我只是参考这个流程图,它让我重回正轨。

在查看整个图片后,我已经理解了另一件事。通过流程图,a-friend函数只是检查seen是否包含任何值(尽管它在#f中有值seen时返回它#tseen为空时,{} {{1}},这可能会造成混淆。

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

答案 4 :(得分:1)

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

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

首先,我在这里复制代码,以便能够看到对应而不会滚动太多:

-

让我们尝试分解问题,看看究竟发生了什么。

  • 案例1:

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

这是一个微不足道的案例,它永远不会循环。

现在有趣的案例:

  • 案例2:

(multirember&co 'a
                '()
                (lambda (x y) (list x y)))

is the same as    

(let ((col (lambda (x y) (list x y))))
  (col '() '()))

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

  • 案例3

(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 '() '()))

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


所有其他情况都是这三种情况中的一种情况的组合,通过添加新图层可以清楚地看出每种情况如何起作用。

答案 5 :(得分:1)

让我们使用一些等式伪代码,为清楚起见省略了一些括号(因此,我们为调用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 = 1的{​​{1}}。因此,这只是一个三个方程式:

  • 当第二个参数是一个空列表时,“收集器”函数lat = [2,3]会立即输入两个空列表作为其两个参数;

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

  • 否则,在构造的收集器收到coln之后,head元素将被添加到n列表中,并且两者将被进一步输入集电器功能。

换句话说,我们正在处理从递归调用返回的两个结果,如果头部为s则将头部置于第二个结果,如果头部为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演算中,函数也只能有一个结果(即使是一对)。

答案 6 :(得分:0)

我希望这次演练有助于

正如克里斯所说,我已经将newlat / seen重命名为n / s并添加了一个索引。 这本书为这些功能提供了可怕的名字(一位朋友的新朋友最新炒),所以我只保留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))