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


14

私は最近、プログラミング言語のオンラインコースに参加しました。このコースでは、概念の中でも特にクロージャが紹介されました。質問をする前に、このコースに触発された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番目の例では、functionを呼び出すと、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 ())が完了した後も引き続き有効です。


2
クローズオーバーされた変数はガベージコレクションから保護され、クローズされていない変数はガベージコレクションの対象となります。したがって、変数が閉じられているかどうかを確実に追跡できるメカニズムは、変数が占有しているメモリを確実に回収できます。
ロバートハーベイ

3
@btilly:Refcountingは、ガベージコレクターのさまざまな実装戦略の1つにすぎません。この質問のためにGCをどのように実装するかは、実際には関係ありません。
ヨルグWミットタグ

3
@btilly: "true"ガベージコレクションとはどういう意味ですか?Refcountingは、GCを実装するもう1つの方法です。おそらく、再カウントでサイクルを収集するのが難しいため、トレースがより一般的です。(通常は、とにかく別のトレースGCになります。そのため、1つのGCで対応できるのに、なぜ2つのGCを実装する必要があります。)しかし、サイクルを処理する他の方法があります。1)それらを禁止します。2)それらを無視します。(迅速な1回限りのスクリプトの実装を行っている場合は、なぜですか?)3)それらを明示的に検出してみてください。(refcountを利用可能にすることで速度が向上することがわかりました。)
ヨルグW

1
そもそもなぜ閉鎖が必要なのかによります。たとえば、完全なラムダ計算のセマンティクスを実装する場合は、必ずGC期間が必要です。他に方法はありません。クロージャーに遠く似ているが、そのような厳密なセマンティクス(C ++、Delphiなど)に準拠していないものが必要な場合は、必要なことを行い、領域分析を使用し、完全に手動のメモリ管理を使用します。
SKロジック

2
@Mason Wheeler:クロージャーは単なる値であり、一般に、実行時にクロージャーがどのように移動するかを予測することはできません。この意味で、これらは特別なものではなく、文字列やリストなどにも同じことが当てはまります。
ジョルジオ

回答:


14

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

Rustは、オプションのGCを備えたシステム言語であり、最初からクロージャーを使用して設計されました。

他の変数として、錆止めにはさまざまな種類があります。最も一般的なスタッククロージャは、ワンショットで使用するためのものです。スタック上に存在し、何でも参照できます。所有されたクロージャは、キャプチャされた変数の所有権を取得します。彼らはいわゆる「交換ヒープ」に住んでいると思います。これはグローバルなヒープです。彼らの寿命は誰が所有するかにかかっています。マネージクロージャーはタスクローカルヒープ上に存在し、タスクのGCによって追跡されます。ただし、キャプチャの制限についてはわかりません。


1
非常に興味深いリンクとRust言語への参照。ありがとう。+1。
ジョルジオ

1
メイソンの答えも非常に有益だと思うので、答えを受け入れる前にたくさん考えました。これは有益であり、あまり知られていない言語を閉鎖に対する独自のアプローチで引用しているため、これを選択しました。
ジョルジオ

ありがとう。私はこの若い言語に非常に熱心であり、私の興味を共有できることを嬉しく思います。Rustについて聞いた前に、GCなしで安全な閉鎖が可能かどうかは知りませんでした。
バルジャック

9

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

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

ただし、変数の寿命を延ばすという考え方は、クロージャーには必要ないことに注意してください。GCによって持ち込まれただけです。元の安全性ステートメントは、クロージャーが続く限り変数が閉じられるだけであるということです(そして、それが不安定な場合でも、クロージャーが最後に呼び出されるまで生き続けるべきだと言えます)。

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

  1. クローズドオーバー変数のライフタイムを延長します(たとえば、GCのように)
  2. 閉鎖の寿命を制限する

後者は対称的なアプローチです。あまり使用されませんが、Rustのように、地域に対応した型システムがあれば、それは確かに可能です。


7

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

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

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


4
「クロージャーにはガベージコレクションは必要ありません。」:問題は、言語が安全なクロージャーを実施できるようにするために必要かどうかです。私はC ++で安全なクロージャーを書くことができることを知っていますが、言語はそれらを強制しません。キャプチャされた変数の有効期間を延長するクロージャーについては、私の質問の編集を参照してください。
ジョルジオ

1
この質問は、安全な閉鎖のために言い換えることができると思います。
マチューM.

1
タイトルには「安全な閉鎖」という用語が含まれていますが、より良い方法で定式化できると思いますか?
ジョルジオ

1
2番目の段落を修正してください。SMLでは、クロージャはキャプチャされた変数によって参照されるデータの寿命を延ばします。また、変数を割り当てること(バインドを変更すること)はできないが、変更可能なデータは(を介して)持っていることは事実ですref。したがって、クロージャの実装がガベージコレクションに関連しているかどうかを議論することはできますが、上記のステートメントを修正する必要があります。
ジョルジオ

1
@Giorgio:今はどうですか?また、クロージャーはキャプチャされた変数の有効期間を延長する必要がないという私の声明をどのような意味で間違っていますか?可変データについて話すときref、構造を指す参照型(s、配列など)について話します。しかし、値は参照そのものであり、参照するものではありません。あなたが持っている場合はvar a = ref 1、あなたがコピーを作成var b = aし、使用b、それはあなたがまだ使用している意味はa?ありません。「aはい」が指す同じ構造にアクセスできます。これが、これらのタイプがSMLで機能する方法であり、クロージャーとは関係ありません
-user102008

6

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

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

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

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


1
ああ、引数ではなくローカル変数のみをキャプチャすることが可能です!これは合理的で賢いトレードオフのようです!+1
ジョルジオ

1
@Giorgio:varパラメーターである引数だけでなく、引数をキャプチャできます。
メイソンウィーラー

2
また、共有プライベートステートを介して通信する2つのクロージャーを持つこともできなくなります。基本的なユースケースではそれは発生しませんが、複雑なことを行う能力が制限されます。まだ可能なことの素晴らしい例です!
-btilly

3
@btilly:実際には、同じ囲い関数内に2つのクロージャーを配置する場合、それは完全に合法です。最終的には同じファンクターオブジェクトを共有することになり、互いに同じ状態を変更すると、一方の変更が他方に反映されます。
メイソンウィーラー

2
@MasonWheeler:「いいえ。ガベージコレクションは本質的に非決定論的です。特定のオブジェクトが収集されるという保証はありません。発生する場合はもちろんです。しかし、参照カウントは決定論的です。カウントが0になった直後に解放されます。」神話が永続することを聞いたときに毎回1セント硬貨があれば。OCamlには確定的なGCがあります。shared_ptrデストラクタはゼロまでデクリメントするため、C ++スレッドセーフは非決定的です。
ジョンハロップ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.