用continuation生成Javascript代码背后的诀窍是什么

What is the trick behind generating Javascript code with continuations?

本文关键字:诀窍 是什么 背后 代码 continuation 生成 Javascript      更新时间:2023-09-26

我正在寻找一种方法,为Javascript添加一种非常特殊的非抢占式多线程形式。Mozilla的Javascript 1.7支持使用yield的本机协同程序,但我不喜欢使用特定于浏览器的解决方案。我看到有几种基于将带注释的Javascript代码转换为纯Javascript的continuation或coroutines实现。例如StratifiedJS、Narrative Javascript和jwacs。

我不需要一个功能齐全的框架来模拟Javascript异步调用;我只需要它来实现一个非常具体的用法。因此,上面的libs对我来说是一种过度的手段

有人能给我指出这种预处理器使用的基本"技巧"吗?是否有一些特殊的语言破解,以生成一些额外的代码为代价,使Javascript中的延续成为可能?欢迎任何相关参考。

这是延续传递样式

Javascript是Lisp,但作为语法却穿着C.的衣服

因为Javascript的核心是一种函数式语言,所以可能会有一些非常疯狂的技巧,比如延续传递风格。但这些把戏令人头疼。

总之,延续是下一步要做什么的概念——作为可以调用的东西,就像函数一样。我有时也会将延续视为保存的调用帧堆栈:您可以将函数调用堆栈保存为执行状态,然后返回或仅"调用"此状态。

有人证明,通过将代码转换为延续传递样式,您可以获得延续的威力。哇!这真的很令人印象深刻:

仅仅是一个源代码转换,你就拥有了延续的力量。

现在,Javascript的问题在于它的C语法。用C语法进行源代码转换是很困难的。使用Lisp语法会更容易,但仍然乏味且容易出错。

我们很幸运,有一些非常聪明的人为我们做了艰苦的工作。这项艰苦的工作需要使用Javascript解析器,因为这种转换到底意味着什么?总之,这意味着重新排序操作的顺序,使真正完成的事情排在第一位。

f(g(a + x))

添加a + x首先完成,然后函数调用g(),然后是f()。有三个子表达式。在CPS变换中,子表达式的结果被传递给延续。这涉及到创建许多内部助手函数作为临时延续。正如我们将在下面看到的那样,这可能会变得复杂和乏味。

在http://en.wikipedia.org/wiki/Continuation-passing_style函数示例

(define (pyth x y)
  (sqrt (+ (* x x) (* y y))))

转换为

(define (pyth& x y k)
  (*& x x (lambda (x2)
      (*& y y (lambda (y2)
               (+& x2 y2 (lambda (x2py2)
                          (sqrt& x2py2 k))))))))

这对应于Javascript

function pyth(x, y) {
    return Math.sqrt(x * x + y * y);
}

但是*、+和Math.sqrt()不是CPS有意义的函数。

但是,为了示例起见,假设*、+和Math.sqrt()是web服务。这一点非常重要,因为Javascript web服务调用是异步的。每个使用过异步调用的人都知道将它们的结果组合在一起会有多复杂。使用预处理库或生成器,可以更容易地处理异步结果。

因此,让我们以不同的方式编写示例:

function pyth(x, y) {
    return sqrt(add(mul(x, x), mul(y, y)));
}

则CPS转换可能如下所示:

function pyth_cps(x, y, k) {
  mul_cps(x, x, function(x2) {
    mul_cps(y, y, function(y2) {
      add_cps(x2, y2, function(x2py2) {
        sqrt_cps(x2py2, k);
      })
    })
  });
}

我们看到生成的代码被从内到外撕毁,变得完全不可读。每个函数都被转换。它们都有一个神奇的参数k。这就是延续。在javascript中,它是一个获取操作结果的函数。调用调用堆栈k的某个深处。在我们的例子中,这里没有显示sqrt()的CPS转换。

还要注意,CPS转换的函数永远不会返回。他们只是用计算的结果来调用continuation。这可能会导致堆栈耗尽。所有Javascript CPS转换器都需要处理此问题。在Scheme中,这是不必要的,因为所有调用都是尾部调用。尾部调用不需要额外的调用框架。在Javascript中,需要蹦床或类似的技术。与其直接调用continuation,不如调用一个助手并将结果和continuation传递给它。助手在一个无休止的循环中运行,总是调用和返回,避免了堆栈耗尽。

那么,为什么这个CPS给了我们延续的力量呢?这是因为延续只是下一步要做的事情。如果我们总是将下一步要做的事情的概念作为附加参数k,并总是将当前表达式的结果传递给它,那么我们已经在代码中实现了这个概念。然而,正如我们所看到的,这种"总是随身携带"是乏味的。

这是一个巨大的代价,即使我们让源代码预处理器来做艰苦的工作。我们为什么要使用continuation?可以抽象控制流。Seaside是一个web应用程序框架,它使用continuations来抽象浏览器的无状态请求流。用户交互可以简洁地建模——不再考虑请求,而是考虑交互流。这只是延续力量的众多例子之一。这种力量对许多人来说似乎也很奇怪,有些可怕。