【问题标题】:Understanding shift/reset in Racket了解 Racket 中的移位/重置
【发布时间】:2017-02-10 17:32:38
【问题描述】:

我在球拍中展示了foldr 的两个幼稚实现

第一个缺少适当的尾调用,并且对于较大的 xs 值存在问题

(define (foldr1 f y xs)
  (if (empty? xs)
      y
      (f (car xs) (foldr1 f y (cdr xs)))))

(foldr1 list 0 '(1 2 3))
; => (1 (2 (3 0))

第二个使用带有延续的辅助函数来实现正确的尾调用,使其可以安全地用于 xs 的大值

(define (foldr2 f y xs)
  (define (aux k xs)
    (if (empty? xs)
        (k y)
        (aux (lambda (rest) (k (f (car xs) rest))) (cdr xs))))
  (aux identity xs))

(foldr2 list 0 '(1 2 3))
; => (1 (2 (3 0)))

racket/control我看到球拍支持一流的延续。我想知道使用shiftreset 来表达foldr 的第二个实现是否可能/有益。我玩了一会儿,结果我的大脑就彻底翻了。

请提供详尽的解释以及任何答案。我在这里寻求大局的理解。

【问题讨论】:

  • 第一个看起来和球拍中提供的一样。如果球拍不需要复杂化,你为什么需要?
  • @Sylwester 我不想专注于foldr 本身,而是更多地关注如何使用不同的技术通过适当的尾调用来表达过程。我只是以foldr 为例。

标签: functional-programming racket continuations delimited-continuations


【解决方案1】:

免责声明:

  1. 你试图解决的foldr的“问题”实际上是它的主要特征。
  2. 从根本上说,您不能轻松地反向处理列表,您能做的最好的事情就是先将其反向。从本质上讲,您使用 lambda 的解决方案与递归没有什么不同,只是不是在堆栈上累积递归调用,而是在许多 lambda 中显式地累积它们,所以唯一的好处是不受堆栈的限制大小,您可以尽可能多地使用内存,因为 lambda 很可能是在堆上分配的,而权衡是您现在为每个“递归调用”执行动态内存分配/释放。

现在,让我们来看看实际的答案吧。


让我们尝试实现foldr,记住我们可以使用延续。这是我的第一次尝试:

(define (foldr3 f y xs)
  (if (empty? xs)
    y
    (reset 
      (f (car xs) (shift k (k (foldr3 f y (cdr xs))))))))
  ; ^ Set a marker here.
  ;    ^ Ok, so we want to call `f`.
  ;               ^ But we don’t have a value to pass as the second argument yet.
  ;                 Let’s just pause the computation, wrap it into `k` to use later...
  ;                 And then resume it with the result of computing the fold over the tail.

如果您仔细查看这段代码,您会发现它与您的 foldr 完全相同——即使我们“暂停”计算,我们也会立即恢复它并将递归调用的结果传递给它,而且这种构造当然不是尾递归的。

好的,那么看起来我们需要确保我们不立即恢复它,而是先执行递归计算,然后然后用递归计算结果恢复暂停的计算。让我们重新编写函数以接受一个延续,并在它实际计算出所需的值后调用它。

(define (foldr4 f y xs)
  (define (aux k xs)
    (if (empty? xs)
      (k y)
      (reset
        (k (f (car xs) (shift k2 (aux k2 (cdr xs))))))))
  (reset (shift k (aux k xs))))

这里的逻辑和之前的版本类似:在if的非平凡分支中,我们设置了一个reset标记,然后开始计算表达式,就好像我们拥有了我们需要的一切一样;然而,实际上,我们还没有列表尾部的结果,所以我们暂停计算,将其“打包”到k2,并执行(这次是尾部)递归调用,说“嘿,当你已经得到了你的结果,继续这个暂停的计算”。

如果你分析这段代码是如何执行的,你会发现它绝对没有魔法,它的工作原理是在遍历列表时将一个延续“包装”到另一个延续,然后,一旦它到达末尾,延续被“解包”并以相反的顺序一一执行。事实上,这个函数的作用与foldr2 的作用完全一样——区别只是语法上的:reset/shift 模式不是创建显式的 lambda,而是允许我们立即开始写出表达式,然后在有人说“等一下,我还没有这个值,让我们在这里暂停并稍后返回”......但在引擎盖下它最终会创建与 lambda 相同的闭包!

我相信,没有比这更好的列表了。

另一个免责声明:我没有实现 reset/shift 的有效 Scheme/Racket 解释器,所以我没有测试这些功能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-08
    相关资源
    最近更新 更多