了解 Racket 中的 shift/reset
Understanding shift/reset in Racket
我在 racket
中展示了 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
我发现 racket 支持 first-class continuations。我在想是不是possible/beneficial用shift
和reset
来表达foldr
的第二种实现。我玩了一会儿,结果我的脑子翻了个底朝天。
请提供任何答案的详尽解释。我在这里寻求全局理解。
免责声明:
- 你要解决的
foldr
的“问题”其实就是它的主要特点
- 从根本上说,您不能轻易地反向处理列表,最好的办法就是先反向处理它。您使用 lambda 的解决方案本质上与递归没有什么不同,只是您不是在堆栈上累积递归调用,而是在许多 lambda 中显式地累积它们,所以唯一的好处是不受堆栈限制大小,您可以使用尽可能多的内存,因为 lambda 很可能在堆上分配,并且权衡是您现在为每个“递归调用”执行动态内存allocations/deallocations。
现在,抛开这个问题,进入真正的答案。
让我们尝试实现 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
的non-trivial分支我们设置了一个reset
标记,然后开始计算表达式,就好像我们已经我们需要的一切;然而,实际上,我们还没有列表尾部的结果,所以我们暂停计算,将其“打包”到 k2
,并执行(这次是尾部)递归调用说“嘿,当你得到结果时,恢复这个暂停的计算。
如果你分析这段代码是如何执行的,你会发现它绝对没有魔法,它只是通过在遍历列表时将一个延续“包装”到另一个延续,然后,一旦它到达最后,continuations被“展开”并以相反的顺序一个一个地执行。事实上,这个函数的作用与 foldr2
的作用完全相同——区别仅在于句法:reset
/shift
模式允许我们开始写出立即表达,然后在某个时候说“等一下,我还没有这个值,让我们在这里暂停,稍后 return”......但在幕后它最终创建了相同的闭包lambda
会!
我相信,没有比这更好的列表了。
另一个免责声明:我没有实现 reset
/shift
的工作 Scheme/Racket 解释器,所以我没有测试这些功能。
我在 racket
中展示了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
我发现 racket 支持 first-class continuations。我在想是不是possible/beneficial用shift
和reset
来表达foldr
的第二种实现。我玩了一会儿,结果我的脑子翻了个底朝天。
请提供任何答案的详尽解释。我在这里寻求全局理解。
免责声明:
- 你要解决的
foldr
的“问题”其实就是它的主要特点 - 从根本上说,您不能轻易地反向处理列表,最好的办法就是先反向处理它。您使用 lambda 的解决方案本质上与递归没有什么不同,只是您不是在堆栈上累积递归调用,而是在许多 lambda 中显式地累积它们,所以唯一的好处是不受堆栈限制大小,您可以使用尽可能多的内存,因为 lambda 很可能在堆上分配,并且权衡是您现在为每个“递归调用”执行动态内存allocations/deallocations。
现在,抛开这个问题,进入真正的答案。
让我们尝试实现 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
的non-trivial分支我们设置了一个reset
标记,然后开始计算表达式,就好像我们已经我们需要的一切;然而,实际上,我们还没有列表尾部的结果,所以我们暂停计算,将其“打包”到 k2
,并执行(这次是尾部)递归调用说“嘿,当你得到结果时,恢复这个暂停的计算。
如果你分析这段代码是如何执行的,你会发现它绝对没有魔法,它只是通过在遍历列表时将一个延续“包装”到另一个延续,然后,一旦它到达最后,continuations被“展开”并以相反的顺序一个一个地执行。事实上,这个函数的作用与 foldr2
的作用完全相同——区别仅在于句法:reset
/shift
模式允许我们开始写出立即表达,然后在某个时候说“等一下,我还没有这个值,让我们在这里暂停,稍后 return”......但在幕后它最终创建了相同的闭包lambda
会!
我相信,没有比这更好的列表了。
另一个免责声明:我没有实现 reset
/shift
的工作 Scheme/Racket 解释器,所以我没有测试这些功能。