如何以动态语言(如JavaScript)实现延续?

时间:2019-06-06 07:26:54

标签: javascript scheme lisp continuations callcc

我需要一些技巧来实现并在JavaScript中实现双唇的延续(我的Lisp几乎类似于方案,除了没有延续和TOC之外)。

这是我的评估函数:

function getFunctionArgs(rest, { env, dynamic_scope, error }) {
    var args = [];
    var node = rest;
    markCycles(node);
    while (true) {
        if (node instanceof Pair && !isEmptyList(node)) {
            var arg = evaluate(node.car, { env, dynamic_scope, error });
            if (dynamic_scope) {
                arg = unpromise(arg, arg => {
                    if (typeof arg === 'function' && isNativeFunction(arg)) {
                        return arg.bind(dynamic_scope);
                    }
                    return arg;
                });
            }
            args.push(arg);
            if (node.haveCycles('cdr')) {
                break;
            }
            node = node.cdr;
        } else {
            break;
        }
    }
    return resolvePromises(args);
}
// -------------------------------------------------------------------------
function evaluateMacro(macro, code, eval_args) {
    if (code instanceof Pair) {
        //code = code.clone();
    }
    var value = macro.invoke(code, eval_args);
    return unpromise(resolvePromises(value), function ret(value) {
        if (value && value.data || !value || selfEvaluated(value)) {
            return value;
        } else {
            return quote(evaluate(value, eval_args));
        }
    });
}
// -------------------------------------------------------------------------
function evaluate(code, { env, dynamic_scope, error = () => {} } = {}) {
    try {
        if (dynamic_scope === true) {
            env = dynamic_scope = env || global_env;
        } else if (env === true) {
            env = dynamic_scope = global_env;
        } else {
            env = env || global_env;
        }
        var eval_args = { env, dynamic_scope, error };
        var value;
        if (isNull(code)) {
            return code;
        }
        if (isEmptyList(code)) {
            return emptyList();
        }
        if (code instanceof Symbol) {
            return env.get(code, { weak: true });
        }
        var first = code.car;
        var rest = code.cdr;
        if (first instanceof Pair) {
            value = resolvePromises(evaluate(first, eval_args));
            if (isPromise(value)) {
                return value.then((value) => {
                    return evaluate(new Pair(value, code.cdr), eval_args);
                });
                // else is later in code
            } else if (typeof value !== 'function') {
                throw new Error(
                    type(value) + ' ' + env.get('string')(value) +
                        ' is not a function while evaluating ' + code.toString()
                );
            }
        }
        if (first instanceof Symbol) {
            value = env.get(first, { weak: true });
            if (value instanceof Macro) {
                var ret = evaluateMacro(value, rest, eval_args);
                return unpromise(ret, result => {
                    if (result instanceof Pair) {
                        return result.markCycles();
                    }
                    return result;
                });
            } else if (typeof value !== 'function') {
                if (value) {
                    var msg = `${type(value)} \`${value}' is not a function`;
                    throw new Error(msg);
                }
                throw new Error(`Unknown function \`${first.name}'`);
            }
        } else if (typeof first === 'function') {
            value = first;
        }
        if (typeof value === 'function') {
            var args = getFunctionArgs(rest, eval_args);
            return unpromise(args, function(args) {
                var scope = dynamic_scope || env;
                var result = resolvePromises(value.apply(scope, args));
                return unpromise(result, (result) => {
                    if (result instanceof Pair) {
                        return quote(result.markCycles());
                    }
                    return result;
                }, error);
            });
        } else if (code instanceof Symbol) {
            value = env.get(code);
            if (value === 'undefined') {
                throw new Error('Unbound variable `' + code.name + '\'');
            }
            return value;
        } else if (code instanceof Pair) {
            value = first && first.toString();
            throw new Error(`${type(first)} ${value} is not a function`);
        } else {
            return code;
        }
    } catch (e) {
        error && error(e, code);
    }
}

注意:

// unpromise and resolvePromises is just used ot unwrap any promise
// inside list and return new promise for whole expression if found
// any promise and not found it just return value as is
// markCycles is used to prevent of recursive printing of list cycles
// if you create graph cycles using `set-cdr!` or `set-car!`

在评估表达式的延续性时是否需要创建堆栈?我怎样才能做到这一点?我以为我以某种方式创建了类Continuation,它将以两种模式进行填充:一种是在可以像宏一样被调用时填充;另一种是等待评估由需要执行的代码填充,我不确定还有我应该如何处理而不评估调用连续性的表达式之前的代码:

(* 10 (cont 2))

(* 10 x)需要被忽略

我也不确定应该如何创建call/cc作为函数。它是否应该返回一些中间数据结构并将其参数存储在该数据结构中,以便可以通过具有继续功能的评估来调用它?

'call/cc': function(lambda) {
   return new CallCC(lambda);
}

并且如果eval找到CallCC的实例,它将继续(不确定如何)使用

if (value instanceof CallCC) {
   value.call(new Continuation(stack));
}

这是您要怎么做?所以通常我的问题是关于堆栈的。需要继续吗?如果需要,那么应如何创建?

我发现这篇文章Writing a Lisp: Continuations显示了如何实现延续,但是由于它在Haskell中,所以很难理解。

2 个答案:

答案 0 :(得分:2)

在解释器中实现延续的一种方法是使该解释器使用其自己的显式堆栈进行函数调用/返回和参数传递。如果您使用宿主语言的堆栈,并且该语言没有延续性,那么事情就很难了。

如果使用自己的显式堆栈,则可以将其转换为“意大利面条堆栈”以继续。 “意大利面条堆栈”与词汇环境的常见表示形式非常相似。它包含框架,这些框架指向父框架。捕获连续性等于保留指向此类框架的指针和一些代码。恢复延续或多或少意味着将堆栈恢复到该帧,并在代码中执行到该点。语言的解释器不会递归。当解释的语言嵌套或递归时,解释器将进行迭代,仅压入并弹出显式堆栈以跟踪状态。

另一种方法是使用线性堆栈,但在进行连续操作时将其复制。要恢复继续,您可以从复制的快照还原整个堆栈。这对于定界的连续很有用,这样可以避免复制整个堆栈,而仅复制定界的那一部分(并将其还原到现有堆栈的顶部,而不是通过替换)。我已经使用使用基础C堆栈的语言实现了定界的延续。将C堆栈的一部分memcpy-d放入驻留在堆中的对象中。恢复连续性后,该保存的堆栈段将在当前堆栈的顶部爆炸。当然,必须调整指针,因为该段现在位于不同的地址,并且必须挂接“悬空电缆”才能将该堆栈段正确地集成到堆栈中。

如果通过编译为CPS(连续传递样式)来对待一种语言,则连续弹出是免费的。每个函数都有一个隐式的隐藏参数:它已收到的延续。函数返回被编译为对该延续的调用。如果函数中的代码块需要计算当前延续,则只需用传入的延续(本地部分返回时发生的未来计算)组成一个小的局部计算未来(可表示为lambda)。亨利·贝克(Henry Baker)根据观察发现,在CPS下,由于没有函数返回(将编译返回到对连续的尾部调用),因此永远不会重新使用旧的堆栈框架。可以只允许堆栈增长,当堆栈达到极限时,可以将其“重绕”回顶部。养鸡计划实施了这一概念;如果您对延续性感兴趣,则值得调查。 Chicken Scheme编译为使用常规C堆栈的C代码。但是,生成的函数永远不会返回:它们通过调用延续来模拟返回,因此堆栈会不断增长。更令人着迷的是,我们通常理解为动态材料的对象也是从堆栈中分配的。由于什么也没有返回,因此这些对象是安全的。每当达到某个特定的堆栈限制时,堆栈上所有仍处于活动状态的对象就会移到堆中,并且堆栈指针会重新绕回顶部。

答案 1 :(得分:1)

首先。所有语言都有延续。当您执行7 + n * 5时,JavaScript会将其重新排序为mul_k(n, 5, (_v) => (add _v 7 k),其中k是执行该表达式之后发生的所有事情的函数。

现在mul_k的第一个参数是一个继续。除了一点思考,这没什么可怕的。您实际上再也不会返回任何东西。每次“返回”都传递给它的延续,这总是一个尾声。

本身不是尾部递归的递归函数将在每个步骤中产生一个新的闭包,并在下一个步骤中嵌套。这些都存储在堆中,因此非尾递归函数会成为带有大量嵌套闭包的尾调用。

这是一个小例子:

(define (get-c) (call/cc (lambda (cont) cont)))

(let ((example (get-c)))
  (displayln example)
  (if (procedure? example)
      (example 10)
      "done"))

让我们想象这就是整个程序。让我们将其编写为JavaScript。

// simple CPS version of displayln
const displayln = (arg, k) k(console.log(arg));

// simple CPS version of procedure?
const isProcedure = (arg, k) k(arg instanceOf Function);

// Simple CPS version of call/cc
const callCC = (userFun, callCCK) 
  => userFun((result, actualK) => callCCK(result), callCCK);

// simple top level continutation. Not a CPS function.
const kTopLevel = console.log;

// the user function get-c transformed into CPS
const getC = getCK => callCC((cont, k) => k(cont), getCK);

// the let code transformed into CPS
getC((example) => // c1
  displayln(example, (_undefined) => // c2 
    isProcedure(example, (result) => // c3
      result ? example(10, kTopLevel) : kTopLevel("done"))

这是正在发生的事情:

  • getC获得整个延续(c1)并调用callCC
  • callCC调用c1并以(result, _) => c1(result)的身份获得example
  • display打印example(续集),将其未定义的返回值传递给其续集c2
  • isPorcedure上的example,它是延续,它将true作为result传递到延续c3
  • 处于true时会调用example(10, kTopLevel),它变成c1(10),因此以10为例
  • display打印èxample, the number 10 , passes its underfined return value to its continuation c2`
  • example上的
  • isProcedure将false作为result传递到延续c3
  • 成为false会调用TopLevel("dome")
  • TopLevel打印“完成”

如您所见。只要将代码在执行之前转换为CPS,call/cc就容易实现。 JavaScript非常支持CPS。

为了帮助您将代码转换为CPS,Matt Might在自己的论文中countless times做到了。在该列表中查找继续。他甚至已经完成了in JavaScript

现在,如果需要解释您的实现,则可以在Scheme中进行相同的转换并解释它而不是用户代码。如果您有连续障碍,则只需从call/cc到障碍之间的CPS。