1

匿名関数に関するウィキペディアの記事から取った、この修正されたコード例があります。

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

main()
{
    a = comp(10);

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

スクリプト言語に無名関数を追加するのはそれほど難しいことではありません。通常の方法で関数コードを追加するだけの場合ですが、その関数にアクセスする唯一の方法は、割り当てられた変数を介することです。

上記のクロージャーの例の問題は、無名関数の関数本体が (一般的な意味で) クロージャーが呼び出された時点で無効な (または無効になる可能性のある) メモリ位置を参照することです。

私はすでに、この問題に対する 2 つの潜在的な解決策を考えています。この機能を自分の言語に追加する前に、まずいくつかの推奨事項を取得したいだけです。

4

4 に答える 4

2

最も賢明な方法についてはわかりませんが、 Lua 5.0 の実装 で Lua がクロージャーを実装する方法について読むことができます。スライドもご覧ください。

クロージャーの Lua 実装の主なポイントは、上位値または外部ローカル変数の効率的な処理です。これにより、Lua は完全なレキシカル スコープをサポートできるようになりました。このサポートが Lua の設計でどのように進化したかについては、HOPL III の論文The Evolution of Luaを参照してください。

于 2011-12-20T21:35:45.887 に答える
1

あなたの質問はあまり具体的ではなかったため、一般的なアルゴリズムでしか回答できません。また、これが「最も賢明な」方法であり、単に機能する方法であるとは主張しません。

まず、問題空間を定義する必要があります。

スクリプト言語にはローカル変数があります (Lua 用語を使用): グローバルではない変数です。これらは、ラムダによって理論的にキャプチャできる変数です。

ここで、スクリプト言語にローカル変数を動的に選択する方法がないと仮定しましょう。つまり、構文ツリーを調べるだけで、次のことがわかります。

  1. 関数によってキャプチャされるローカル変数。
  2. 関数によってキャプチャされないローカル変数。
  3. スコープ外のローカル変数をキャプチャする関数。
  4. スコープ外のローカル変数をキャプチャしない関数はどれですか。

この情報を考慮して、ローカル変数は、純粋なローカル変数とキャプチャされたローカル変数の2 つのグループに分割されます。私はこれらを「純粋な地元の人々」と「捕らえられた地元の人々」と呼びます。

純粋なローカルは、より適切な用語が必要なため、レジスターです。バイトコードにコンパイルする場合、純粋なローカルは最も扱いが簡単です。それらは特定のスタック インデックスであるか、特定のレジスタ名です。ただし、スタック管理を行っている場合、純粋なローカルには特定のスコープ内の固定の場所が割り当てられます。JIT の力を利用している場合、これらはレジスターになるか、それに最も近いものになります。

キャプチャされたローカルについて最初に理解する必要があるのは、メモリ マネージャーで管理する必要があるということです。それらは現在のコール スタックとスコープから独立して存在するため、それらをキャプチャする関数によって参照される独立したオブジェクトである必要があります。これにより、複数の関数が同じローカルをキャプチャできるため、相互にプライベート データを参照できます。

したがって、キャプチャされたラムダを含むスコープに入ると、その特定のスコープの一部であるキャプチャされたすべてのローカルを含むメモリの一部が割り当てられます。例えば:

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

関数のルート スコープにcompは 2 つのローカル変数があります。二人とも捕まる。したがって、キャプチャされたローカルの数は 2 で、純粋なローカルの数は 0 です。

したがって、コンパイラは (バイト コードに対して) 純粋なローカルに 0 レジスタ/スタック変数を割り当て、2 つの変数を含む独立したオブジェクトを割り当てます。ガベージ コレクションを使用していると仮定すると、ガベージ コレクションを存続させるには、ガベージ コレクションを参照する何かが必要になります。それは簡単です。スクリプトから直接アクセスできないレジスタ/スタックの場所で参照します。実際には、レジスタ/スタック変数を割り当てますが、スクリプトはそれに直接触れることはできません。

では、 が何をするのか見てみましょうlambda。関数を作成します。繰り返しますが、この関数はスコープ外のいくつかの変数をキャプチャすることがわかっています。そして、それがどの変数をキャプチャするかを知っています。2 つの変数がキャプチャされていることがわかりますが、これら 2 つの変数が同じ独立したメモリ ブロックから取得されていることもわかります。

つまりlambda、バイトコードへの参照と、それに関連付けられている変数への参照を持つ関数オブジェクトを作成することです。バイトコードはその参照を使用して、キャプチャされた変数を取得します。コンパイラは、関数に対して純粋にローカルな変数 (引数xなど) と、外部でキャプチャされたローカル変数 (しきい値など) を認識しています。したがって、各変数にアクセスする方法を理解できます。

lambda完了すると、関数オブジェクトが返されます。この時点で、キャプチャされた変数は、ラムダ関数とスタック(関数の現在のスコープ) の 2 つによって参照されます。ただし、終了するreturnと、現在のスコープは破棄され、以前に参照されていたものはすべて参照されなくなります。したがって、関数オブジェクトを返すとき、ラムダ関数のみがキャプチャされた変数への参照を持ちます。

ただし、これはすべてかなり複雑です。はるかに単純な実装は、すべてのローカル変数を効果的にキャプチャすることです。すべてのローカル変数はキャプチャされたローカルです。これを行うと、コンパイラはより単純になります (そしておそらくより高速になります)。新しいスコープに入ると、そのスコープのすべてのローカルがメモリ ブロックに割り当てられます。関数が作成されると、使用するすべての外部スコープを参照します (存在する場合)。また、スコープが終了すると、割り当てられたローカルへの参照が削除されます。誰もそれを参照していない場合は、メモリを解放できます。

とてもシンプルで簡単です。

于 2011-12-20T23:34:41.020 に答える
1

これを C++ (ラムダなし) に変換すると、次のようになります。

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 に答える
0

Lua で使用される上位値について読んでいます。クロージャーと完全な字句スコープを扱うための同様のシステムを実装しようとしています。トリッキーな部分は、必要に応じて、コンパイラに close コマンドを正しい場所に配置させることです。

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 に答える