用脚本语言实现lambdas

时间:2011-12-20 17:32:09

标签: c++ scripting lambda closures scripting-language

我有一个修改过的代码示例,我从维基百科的匿名函数文章中获取。

comp(threshold)
{
    return lambda(x)
    {
        return x < threshold;
    };
}

main()
{
    a = comp(10);

    lib::print( a(5) );
}

匿名函数不应该太难添加到我的脚本语言中。它应该只是以正常方式添加功能代码的情况,除了访问该功能的唯一方法是通过它所分配的变量。

上面的闭包示例的问题是匿名函数的函数体引用(在一般意义上)在调用闭包时无效(或将会)的内存位置。

我已经有两种可能解决问题的方法了;我想在我尝试将此功能添加到我的语言之前先得到一些建议。

4 个答案:

答案 0 :(得分:2)

我不知道大多数明智的方法,但你可以阅读Lua如何在The implementation of Lua 5.0中实现闭包。另请参阅slides

Lua实现闭包的要点是有效处理 upvalues 或外部局部变量。这使得Lua能够支持完整的词法范围。有关Lua设计中这种支持如何演变的说明,请参阅HOPL III论文The Evolution of Lua

答案 1 :(得分:1)

如果我将其翻译成C ++(没有lambdas),它将如下所示:

struct comp_lamda_1 {
    int threshold;
    comp_lamda_1(int t) :threshold(t) {}
    bool operator()(int x) {
        return x < threshold;
    };
};

comp_lambda_1 comp(int threshold)
{
    return comp_lamda_1(threshold);
}

int main()
{
    auto a = comp(10);
    std::cout << a(5);
}

这表明解释器不应将匿名函数视为独立函数,而应将其视为函数 - 对象,它具有捕获所需变量的成员。

(很明显,关键是comp_lamda_1是一个函数对象,我知道你不是要求上述代码的C ++翻译)

答案 2 :(得分:1)

由于您的问题不是非常具体,因此只能通过一般算法来回答。我也没有声称这是“最明智的”方式,只是一种有效的方式。

首先,我们需要定义问题空间。

您的脚本语言具有局部变量(使用Lua术语):非全局变量。这些变量理论上可以由lambda捕获。

现在,让我们假设您的脚本语言没有办法动态选择局部变量。这意味着,只需检查语法树,就可以看到以下内容:

  1. 函数捕获哪些局部变量。
  2. 某个函数捕获了哪些 的局部变量。
  3. 哪些函数捕获范围之外的局部变量。
  4. 哪些函数捕获其范围之外的局部变量。
  5. 根据这些信息,局部变量现在分为两组:局部变量和捕获的局部变量。我将这些称为“纯粹的当地人”和“被捕的当地人”。

    由于缺乏更好的术语,纯粹的本地人会注册。当您编译为字节代码时,纯本地语言是最简单的处理。它们是特定的堆栈索引,或者它们是特定的寄存器名称。但是,您正在进行堆栈管理,纯粹的本地人在特定范围内被分配了固定位置。如果你挥舞着JIT的力量,那么它们将成为寄存器,或者是最接近JIT的东西。

    您需要了解捕获的本地化的第一件事是:它们必须由您的内存管理器管理。它们独立于当前的调用堆栈和范围而存在,因此,它们需要是由捕获它们的函数引用的独立对象。这允许多个函数捕获相同的本地,因此引用彼此的私有数据。

    因此,当您输入包含捕获的lambda的范围时,您将分配一块内存,其中包含属于该特定范围的所有捕获的本地。例如:

    comp(threshold)
    {
        local data;
        return lambda(x)
        {
            return x < (threshold + data);
        };
    }
    

    comp函数的根范围有两个局部变量。它们都被捕获了。因此,捕获的本地数量为2,纯本地数量为零。

    因此,您的编译器(到字节代码)将为纯本机分配0个寄存器/堆栈变量,它将分配一个包含两个变量的独立对象。假设您正在使用垃圾收集,您将需要一些东西来引用它以便它继续存在。这很容易:您在寄存器/堆栈位置引用它,脚本无法直接访问它。实际上,你确实分配了一个寄存器/堆栈变量,但脚本不能直接触摸它。

    现在,让我们来看看lambda的作用。它创造了一个功能。同样,我们知道该函数捕获了其范围之外的一些变量。我们知道它捕获的变量。我们看到它捕获了两个变量,但我们也发现这两个变量来自同一个独立的内存块。

    因此lambda所做的是创建一个函数对象,该对象具有对某些字节码的引用以及对与其关联的变量的引用。字节码将使用该引用来获取其捕获的变量。您的编译器知道哪些变量是函数的纯本地变量(如参数x),哪些是外部捕获的本地变量(如阈值)。所以它可以弄清楚如何访问每个变量。

    现在,当lambda完成时,它会返回一个函数对象。此时,捕获的变量由两部分引用:lambda函数和堆栈:函数的当前范围。但是,当return完成时,将销毁当前作用域,并且不再引用先前引用的所有内容。因此,当它返回函数对象时,只有lambda函数具有对捕获变量的引用。

    但这一切都相当复杂。一个更简单的实现就是让所有局部变量得到有效捕获; 所有局部变量都是捕获的本地变量。如果你这样做,那么你的编译器可以更简单(并且可能更快)。输入新范围时,该范围的所有本地都将分配在内存块中。创建函数时,它会引用它使用的所有外部作用域(如果有)。退出范围时,它会删除对其分配的本地的引用。如果没有其他人正在引用它,那么可以释放内存。

    这非常简单明了。

答案 3 :(得分:0)

我一直在读Lua中使用的upvalues。我将尝试实现一个类似的系统来处理闭包和完整的词法范围。棘手的部分是让编译器根据需要将close命令放在正确的位置。

function()
{
    a = 6, b;

    {
        local c = 5;

        b = lambda() { return a*c; };

        // close c in closure;
    }

    b();

    // close a in closure
}