1

我有这个修改过的代码示例,我取自关于匿名函数的维基百科文章。

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

main()
{
    a = comp(10);

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

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

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

在我的脑海中,我已经有两种可能的解决方案;在我尝试将此功能添加到我的语言之前,我只想先获得一些建议。

4

4 回答 4

2

我不知道最明智的方法,但您可以在Lua 5.0 的实现中了解 Lua 如何实现闭包。另见幻灯片

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

于 2011-12-20T21:35:45.887 回答
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++ 翻译)

于 2011-12-20T23:24:29.873 回答
1

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

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

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

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

  1. 函数捕获了哪些局部变量。
  2. 函数未捕获哪些局部变量。
  3. 哪些函数捕获其范围之外的局部变量。
  4. 哪些函数不会捕获其范围之外的局部变量。

鉴于此信息,局部变量现在分为两组:局部变量和捕获的局部变量。我将这些称为“纯当地人”和“被俘虏的当地人”。

纯粹的本地人,为了更好的术语,是注册人。当你编译成你的字节码时,纯本地是最容易处理的。它们是特定的堆栈索引,或者它们是特定的寄存器名称。无论您如何进行堆栈管理,都会为纯本地人分配特定范围内的固定位置。如果您使用 JIT 的力量,那么这些将成为寄存器,或者最接近可能的东西。

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

因此,当您进入一个包含捕获的 lambda 的作用域时,您将分配一块内存,其中包含属于该特定作用域的所有捕获的局部变量。例如:

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

函数的根作用域comp有两个局部变量。两人都被俘虏了。因此,捕获的局部数为 2,纯局部数为零。

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

现在,让我们看看有什么lambda作用。它创建了一个函数。同样,我们知道这个函数捕获了一些超出其范围的变量。我们知道它捕获了哪些变量。我们看到它捕获了两个变量,但我们也看到这两个变量来自同一个独立的内存块。

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

现在,当lambda完成时,它返回一个函数对象。此时,捕获的变量被两件事引用:lambda 函数和堆栈:函数的当前范围。然而,当return完成时,当前作用域被销毁,并且之前引用的所有内容都不再被引用。因此,当它返回函数对象时,只有 lambda 函数具有对捕获变量的引用。

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

这非常简单明了。

于 2011-12-20T23:34:41.020 回答
0

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

function()
{
    a = 6, b;

    {
        local c = 5;

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

        // close c in closure;
    }

    b();

    // close a in closure
}
于 2011-12-22T01:08:45.740 回答