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

时间:2012-02-22 09:33:51

标签: javascript code-generation preprocessor continuations coroutine

我正在寻找一种方法来向Javascript添加一种非抢占式多线程的非常特定形式。 Mozilla的Javascript 1.7支持使用yield的本地协同程序,但我不想使用特定于浏览器的解决方案。我看到有几个continuation或coroutines的实现,基于将带注释的Javascript代码转换为普通的Javascript。一些示例包括StratifiedJSNarrative Javascriptjwacs

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

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

1 个答案:

答案 0 :(得分:10)

延续传递风格

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

因为Javascript是一种功能性语言,所以真正的疯狂技巧是可能的,比如延续传递风格。但这些技巧令人头疼。

在摘要中,延续是下一步做什么的概念 - 作为可以调用的东西提供,就像一个函数一样。我有时也会将continuation视为已保存的调用帧堆栈:您可以将一堆函数调用保存为执行状态,然后返回或稍后“调用”此状态。

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

只是源代码转换,并且你有继续的力量。

现在,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))))))))

这对应于Javacript

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转换函数永远不会返回。他们只是用计算结果调用延续。这可能导致堆栈耗尽。所有Javascript CPS变换器都需要处理这个问题。在Scheme中,这不是必需的,因为所有调用都是尾调用。尾调用不需要额外的调用帧。在Javascript中,需要蹦床或类似的技术。而不是直接调用continuation,调用一个帮助器并将结果和延续传递给它。帮助程序在无限循环中运行,始终调用并返回并避免堆栈耗尽。

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

付出的代价很高,即使我们让源代码预处理器做了很多努力。我们为什么要使用延续?抽象控制流是可能的。 Seaside是一个Web应用程序框架,它使用continuation来抽象出浏览器的无状态请求流。用户交互可以简洁地建模 - 人们不再在请求中思考,而是在交互流程中。这只是延续力量的众多例子之一。对于很多人来说,这种力量似乎也很奇怪,有些可怕。