任务、控制流与事件循环(二)
发布于 - 修改于 - 大约需要 3 分钟 - 1408 字再见续体
首先先吐槽一下,continuation 的官方翻译 被称作“继续”,读着总觉得怪怪的。这篇文章中姑且称之为续体。
如 CPS 那篇文章所介绍,续体代表的是一个“后续的计算”。
例如对于 1 + 2 + 3
,在计算完 1 + 2
以后,此时的后续就是 (lambda x (+ x 3))
。
那么问题是,我们应该如何获取续体,而不是显式地定义一个回调,而又该如何定义续体的范围呢?
这就得介绍到 delimited continuation,以及 reset
和 shift
这对操作符了。
这篇博客参考了我的恩师 Oleg Kiselyov 的一篇论文。
shift 与 reset
如果我们有如下程序:
#lang racket
(displayln "Hello World")
(displayln "Hello Again")
那么我们可以通过如下方式获取输出 "Hello World"
以后的续体:
#lang racket
(reset
(displayln "Hello World")
(shift k (k))
(displayln "Hello Again")
)
这段代码和上面的效果是一模一样的。但是当我们调整一下:
#lang racket
(require racket/control)
;; 以下例子省略上述两行
(reset
(displayln "Hello World")
(shift k (void))
(displayln "Hello Again")
)
我们就会看到,欸嘿,"Hello Again"
不见了!这是为什么呢?
shift
这个操作符会接受一个函数,这个函数的参数就是当前节点的续体。那么这个续体具体延续到哪里为止呢?
那就是由 reset
决定的。这个续体有限制的特征也是 delimited 的含义。shift
接受的函数的返回值会返回给 reset
的调用处,例如:
(let ([a (reset (+ 1 2 (shift k 1) 3))])
(displayln a))
它直接把所有的续体都给扔了,并且返回了1。
同理我们可以写出如下反转控制流的例子:
(define (reverted-for ls)
(reset
(for ([i ls])
;; 对于每一个值,我们返回它和剩余控制流,即:遍历并在找到每一个值时 shift
(shift k (list i k)))
(shift k '())))
(define (visit f list)
(let visit-aux ([next (reverted-for list)])
;; 当前内容为当前值与剩余控制流或空列表
(match next
[(list value continuation)
;; 我们访问值、计算控制流,并再次将其 shift 的返回值进行递归运算
(f value)
(visit-aux (continuation))]
[_ (void)])))
(visit displayln '(1 2 3 4)) ;; 输出 1 2 3 4
可以看到,reset
与 shift
十分强大,使得我们可以将通常无法中断的内部迭代器给转换为了外部迭代器。
当然,它们只是实现 delimited continuation 的一对操作符,还有其他的实现方式,这里按下不表。
delimited continuation 与 async await
那么到了这里,有没有觉得 reset
shift
似乎与另一对组合十分相近?
我们前篇提到,我们可以用 Promise
或者说 Future
来对任务进行一个抽象,
将后续任务定义在前序任务完成之后条件执行。而在前篇中,我们都是通过回调实现的,如:
(test-case
"on-success should execute upon a resolved future"
(let-values ([(box) (box #f)]
[(future resolve reject) (make)])
(begin
(resolve 5)
(on-success future (λ (v) (set-box! box v)))
(check-eq? (unbox box) 5))))
那么有没有办法定义更加自然,不依赖回调呢?在任务顺序且只考虑正确的场景下,我们完全可以通过控制流变换,利用 shift
获取当前的续体,
把续体作为回调挂上去,如:
;; 假设 future.rkt 中添加了 bind 与 successful 的实现
;; 其中 successful 基于参数直接构建一个已兑现的 Future
(require "future.rkt")
(define (await future)
(let-values ([(f resolve reject) (make)])
(shift k
(on-success future (λ (v)
(let ([f2 (k v)])
(on-success f2 resolve)
(on-failure f2 reject))))
(on-failure future (λ (v) (reject v)))
f)))
(module+ test
(require rackunit)
;; 如果使用回调
(test-case
"future with callbacks"
(let-values ([(box) (box #f)]
[(future1 resolve1 reject1) (make)])
(let ([future2 (bind future1 (λ (v) (successful (+ v 1))))])
(on-success future2 (λ (v) (set-box! box v)))
(resolve1 5)
(check-equal? (unbox box) 6))))
;; 如果使用 await
(test-case
"future with reset/shift"
(let-values ([(box) (box #f)]
[(future1 resolve1 reject1) (make)])
(let ([future2 (reset
(let ([v (await future1)])
(successful (+ v 1))))])
(on-success future2 (λ (v) (set-box! box v)))
(resolve1 5)
(check-equal? (unbox box) 6)))))
可以看到,这就是一个粗略的 async
await
的雏形,利用 reset
shift
进行控制流变换,自动将后续内容挂载到前序任务之后。
当然,这里的实现并不完善,例如整体返回值依然需要是一个 Future
,没有考虑错误处理等。
总结
通过 reset
shift
这一类控制流变换的运算符,我们获得了强大的能力。我们可以反转迭代器,也可以在执行任务时,如果依赖某个资源,
就将后续控制流收起,等待资源准备好以后执行。在我们的例子中,这个资源是通过 Future
抽象的一个简单的值,
所谓的等待也只是等我们执行完任务的定义后就兑现这个值。
那么问题是,如果这个资源是一个网络请求的响应,或是某个闹铃,那么在等待资源准备好的这段时间,我们难道只是干等着吗? 我们是否可以在这个期间做点别的事呢?那就是我们下一篇的主题:事件循环。