web-dev-qa-db-ja.com

安全なクロージャを実装するためにガベージコレクションは必要ですか?

私は最近、プログラミング言語に関するオンラインコースに参加しました。このコースでは、他の概念の中でも、クロージャーが紹介されました。このコースに触発された2つの例を書き留めてから、質問する前に状況を説明します。

最初の例は、1からxまでの数値のリストを生成するSML関数です。ここで、xは関数のパラメーターです。

_fun countup_from1 (x: int) =
    let
        fun count (from: int) =
            if from = x
            then from :: []
            else from :: count (from + 1)
    in
        count 1
    end
_

SML REPLで:

_val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list
_

_countup_from1_関数は、コンテキストから変数countをキャプチャして使用するヘルパークロージャxを使用します。

2番目の例では、_create_multiplier t_関数を呼び出すと、引数にtを乗算する関数(実際にはクロージャー)が返されます。

_fun create_multiplier t = fn x => x * t
_

SML REPLで:

_- fun create_multiplier t = fn x => x * t;
val create_multiplier = fn : int -> int -> int
- val m = create_multiplier 10;
val m = fn : int -> int
- m 4;
val it = 40 : int
- m 2;
val it = 20 : int
_

したがって、変数mは、関数呼び出しによって返されたクロージャーにバインドされ、自由に使用できるようになりました。

クロージャーがその存続期間を通じて適切に機能するためには、キャプチャーされた変数tの存続期間を延長する必要があります(この例では整数ですが、任意の型の値にすることができます)。私の知る限り、SMLではこれはガベージコレクションによって可能になります。クロージャーは、キャプチャーされた値への参照を保持します。キャプチャされた値は、クロージャーが破棄されたときにガベージコレクターによって後で破棄されます。

私の質問:一般的に、ガベージコレクションは、クロージャーが安全であることを保証するための唯一の可能なメカニズムですか?

または、ガベージコレクションなしでクロージャの有効性を保証できる他のメカニズムは何ですか。キャプチャした値をコピーしてクロージャ内に保存しますか?キャプチャーされた変数が期限切れになった後に呼び出されないように、クロージャー自体の存続時間を制限しますか?

最も人気のあるアプローチは何ですか?

[〜#〜]編集[〜#〜]

上記の例は、キャプチャされた変数をクロージャにコピーすることで説明/実装できるとは思いません。一般に、キャプチャされた変数は任意のタイプにすることができます。それらは非常に大きな(不変の)リストにバインドできます。したがって、実装では、これらの値をコピーすることは非常に非効率的です。

完全を期すために、参照(および副作用)を使用した別の例を次に示します。

_(* Returns a closure containing a counter that is initialized
   to 0 and is incremented by 1 each time the closure is invoked. *)
fun create_counter () =
    let
        (* Create a reference to an integer: allocate the integer
           and let the variable c point to it. *)
        val c = ref 0
    in
        fn () => (c := !c + 1; !c)
    end

(* Create a closure that contains c and increments the value
   referenced by it it each time it is called. *)
val m = create_counter ();
_

SML REPLで:

_val create_counter = fn : unit -> unit -> int
val m = fn : unit -> int
- m ();
val it = 1 : int
- m ();
val it = 2 : int
- m ();
val it = 3 : int
_

したがって、変数は参照によってキャプチャすることもでき、変数を作成した関数呼び出し(create_counter ())が完了した後も存続します。

14
Giorgio

Rustプログラミング言語はこの点で興味深いものです。

Rustはシステム言語であり、オプションのGCがあり、最初から closures で設計されています。

他の変数として、Rustクロージャーにはさまざまなフレーバーがあります。スタッククロージャー、最も一般的なものは、 -shotの使用法。それらはスタック上にあり、何でも参照できます。所有のクロージャーは、取得した変数の所有権を取得します。いわゆる「グローバルヒープであるエクスチェンジヒープ。それらの寿命は、それらを所有するユーザーによって異なります。マネージドクロージャは、タスクローカルヒープ上に存在し、タスクのGCですが、キャプチャの制限についてはわかりません。

14
barjak

残念ながら、GCから始めると、XYシンドロームの犠牲になります。

  • (安全上の理由から)クロージャが必要とする限り、ライブで閉じた変数よりもクロージャが必要です。
  • gCを使用して、これらの変数の寿命を十分に長くすることができます
  • XY症候群:寿命を延ばす他のメカニズムはありますか?

ただし、extendingの考え方よりも、変数の寿命はnecessaryではないことに注意してください。それは単にGCによってもたらされます。元の安全性ステートメントはクローズされた変数は、クロージャーが存在する限り存続する必要があります(そして不安定な場合でも、クロージャーが最後に呼び出されるまで存続する必要があると言えます)。

基本的に、2つのアプローチがあります私が見ることができる(そしてそれらは潜在的に組み合わせることができます):

  1. クローズされた変数の寿命を延ばす(たとえば、GCのように)
  2. 閉鎖の有効期間を制限する

後者は単に対称的なアプローチです。これはあまり使用されませんが、Rustのようにリージョン対応の型システムがある場合は、それが可能です。

9
Matthieu M.

値で変数をキャプチャする場合、ガベージコレクションは安全なクロージャには必要ありません。顕著な例の1つはC++です。 C++には標準のガベージコレクションはありません。 C++ 11のラムダはクロージャです(周囲のスコープからローカル変数をキャプチャします)。ラムダによってキャプチャされた各変数は、値または参照によってキャプチャされるように指定できます。参照によってキャプチャされた場合、それは安全ではないと言えます。ただし、変数が値でキャプチャされる場合、キャプチャされたコピーと元の変数は別個であり、独立した有効期間を持っているため、安全です。

提供したSMLの例では、説明は簡単です。変数は値によってキャプチャされます。変数の値をクロージャーにコピーするだけなので、変数の「存続期間を延長する」必要はありません。これは、MLでは変数を割り当てることができないため可能です。したがって、1つのコピーと多くの独立したコピーの間に違いはありません。 SMLにはガベージコレクションがありますが、クロージャーによる変数のキャプチャーとは関係ありません。

参照(種類)によって変数をキャプチャする場合、安全なクロージャにはガベージコレクションも必要ありません。 1つの例はApple C、C++、Objective-C、およびObjective-C++言語へのブロック拡張です。CおよびC++には標準のガベージコレクションはありません。ブロックはデフォルトで値によって変数をキャプチャしますただし、ローカル変数が__blockを使用して宣言されている場合、ブロックはそれらを「参照によって」見えるようにキャプチャし、安全です。これらは、ブロックが定義されているスコープの後でさえ使用できます。ここで発生するのは__block変数ですは実際には特別な構造であり、ブロックがコピーされると(ブロックをスコープの外で使用するために最初にコピーする必要があります)、それらは__block変数の構造をヒープに「移動」し、ブロックはその構造を管理します記憶、私は参照カウントを通じて信じています。

7
user102008

クロージャーを実装するためにガベージコレクションは必要ありません。 2008年に、ガベージコレクションされていないDelphi言語がクロージャーの実装を追加しました。それはこのように動作します:

コンパイラーは、クロージャーを表すインターフェースを実装する内部でファンクターオブジェクトを作成します。すべての閉じられたローカル変数は、外側のプロシージャのローカルからファンクタオブジェクトのフィールドに変更されます。これにより、ファンクタが存在する限り状態が保持されます。

このシステムの制限は、囲んでいる関数への参照によって渡されたパラメーター、および関数の結果値は、範囲が囲んでいる関数のスコープに制限されているローカルではないため、ファンクターによってキャプチャできないことです。

ファンクタは、クロージャ参照によって参照され、構文シュガーを使用して、インターフェイスではなく関数ポインタのように開発者に見せます。 Delphiの参照カウントシステムをインターフェイスに使用して、ファンクターオブジェクト(およびそれが保持するすべての状態)が必要な限り「生きている」状態を維持し、refcountが0になったときに解放されます。

6
Mason Wheeler