Baru-baru ini saya menghadiri kursus online tentang bahasa pemrograman di mana, di antara konsep-konsep lain, penutupan disajikan. Saya menuliskan dua contoh yang terinspirasi oleh kursus ini untuk memberikan konteks sebelum mengajukan pertanyaan saya.
Contoh pertama adalah fungsi SML yang menghasilkan daftar angka dari 1 hingga x, di mana x adalah parameter fungsi:
fun countup_from1 (x: int) =
let
fun count (from: int) =
if from = x
then from :: []
else from :: count (from + 1)
in
count 1
end
Di SML REPL:
val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list
The countup_from1
Fungsi menggunakan penutupan penolong count
yang menangkap dan menggunakan variabel x
dari konteksnya.
Dalam contoh kedua, ketika saya menjalankan fungsi create_multiplier t
, saya mendapatkan kembali fungsi (sebenarnya, penutupan) yang mengalikan argumennya dengan t:
fun create_multiplier t = fn x => x * t
Di 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
Jadi variabel m
terikat pada penutupan yang dikembalikan oleh pemanggilan fungsi dan sekarang saya bisa menggunakannya sesuka hati.
Sekarang, agar penutupan berfungsi dengan baik sepanjang masa pakainya, kita perlu memperpanjang masa hidup dari variabel yang ditangkap t
(dalam contoh itu adalah bilangan bulat tetapi bisa berupa nilai dari jenis apa pun). Sejauh yang saya tahu, di SML ini dimungkinkan oleh pengumpulan sampah: penutupan menyimpan referensi ke nilai yang ditangkap yang kemudian dibuang oleh pengumpul sampah ketika penutupan dihancurkan.
Pertanyaan saya: secara umum, apakah pengumpulan sampah adalah satu-satunya mekanisme yang mungkin untuk memastikan bahwa penutupan aman (dapat dipanggil selama masa pakainya)?
Atau apakah mekanisme lain yang dapat memastikan validitas penutupan tanpa pengumpulan sampah: Salin nilai yang ditangkap dan simpan di dalam penutupan? Batasi masa berlaku penutupan itu sendiri sehingga tidak dapat dipanggil setelah variabel yang ditangkap telah kedaluwarsa?
Apa pendekatan yang paling populer?
EDIT
Saya tidak berpikir contoh di atas dapat dijelaskan / diimplementasikan dengan menyalin variabel yang ditangkap ke penutupan. Secara umum, variabel yang ditangkap dapat dari jenis apa pun, misalnya mereka dapat terikat pada daftar yang sangat besar (tidak berubah). Jadi, dalam implementasinya akan sangat tidak efisien untuk menyalin nilai-nilai ini.
Demi kelengkapan, berikut adalah contoh lain menggunakan referensi (dan efek samping):
(* 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 ();
Di 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
Jadi, variabel juga bisa ditangkap dengan referensi dan masih hidup setelah pemanggilan fungsi yang membuatnya ( create_counter ()
) telah selesai.
Jawaban:
Bahasa pemrograman Rust menarik pada aspek ini.
Karat adalah bahasa sistem, dengan GC opsional, dan dirancang dengan penutupan sejak awal.
Sebagai variabel lain, penutupan karat datang dalam berbagai rasa. Penutupan tumpukan , yang paling umum, adalah untuk penggunaan sekali pakai. Mereka tinggal di tumpukan dan dapat referensi apa pun. Penutupan yang dimiliki mengambil kepemilikan dari variabel yang ditangkap. Saya pikir mereka hidup di "pertukaran yang disebut", yang merupakan tumpukan global. Umur mereka tergantung pada siapa yang memilikinya. Penutupan yang dikelola langsung di tumpukan tugas-lokal, dan dilacak oleh GC tugas. Saya tidak yakin tentang batasan penangkapan mereka.
sumber
Sayangnya dimulai dengan GC membuat Anda menjadi korban sindrom XY:
Namun, perlu diketahui bahwa gagasan untuk memperpanjang umur variabel tidak diperlukan untuk penutupan; itu hanya dibawa oleh GC; pernyataan keselamatan asli hanya variabel tertutup yang harus hidup selama penutupan (dan bahkan itu goyah, kita bisa mengatakan mereka harus hidup sampai setelah doa terakhir penutupan).
Ada, pada dasarnya, dua pendekatan yang bisa saya lihat (dan mereka berpotensi digabungkan):
Yang terakhir hanyalah pendekatan simetris. Ini tidak sering digunakan, tetapi jika, seperti Rust, Anda memiliki sistem tipe sadar wilayah, maka itu pasti mungkin.
sumber
Pengumpulan sampah tidak diperlukan untuk penutupan yang aman, saat menangkap variabel berdasarkan nilai. Salah satu contoh yang menonjol adalah C ++. C ++ tidak memiliki pengumpulan sampah standar. Lambdas di C ++ 11 adalah penutupan (mereka menangkap variabel lokal dari ruang lingkup sekitarnya). Setiap variabel yang ditangkap oleh lambda dapat ditentukan untuk ditangkap oleh nilai atau dengan referensi. Jika ditangkap dengan referensi, maka Anda dapat mengatakan bahwa itu tidak aman. Namun, jika variabel ditangkap oleh nilai, maka aman, karena salinan yang diambil dan variabel asli terpisah dan memiliki masa hidup yang independen.
Dalam contoh SML yang Anda berikan, mudah dijelaskan: variabel ditangkap oleh nilai. Tidak perlu "memperpanjang umur" dari variabel apa pun karena Anda bisa menyalin nilainya ke dalam penutupan. Ini dimungkinkan karena, dalam ML, variabel tidak dapat ditugaskan. Jadi tidak ada perbedaan antara satu salinan dan banyak salinan independen. Meskipun SML memiliki pengumpulan sampah, itu tidak terkait dengan penangkapan variabel dengan penutupan.
Pengumpulan sampah juga tidak diperlukan untuk penutupan yang aman saat menangkap variabel dengan referensi (jenis). Salah satu contoh adalah ekstensi Apple Blocks ke bahasa C, C ++, Objective-C, dan Objective-C ++. Tidak ada pengumpulan sampah standar di C dan C ++. Blok menangkap variabel dengan nilai secara default. Namun, jika variabel lokal dideklarasikan dengan
__block
, maka blok menangkap mereka yang tampaknya "oleh referensi", dan mereka aman - mereka dapat digunakan bahkan setelah ruang lingkup blok didefinisikan. Apa yang terjadi di sini adalah bahwa__block
variabel sebenarnya adalah struktur khusus di bawahnya, dan ketika blok disalin (blok harus disalin untuk menggunakannya di luar ruang lingkup di tempat pertama), mereka "memindahkan" struktur untuk__block
variabel ke tumpukan, dan blok mengelola ingatannya, saya percaya melalui penghitungan referensi.sumber
ref
). Jadi, oke, orang bisa memperdebatkan apakah pelaksanaan penutupan terkait dengan pengumpulan sampah atau tidak, tetapi pernyataan di atas harus diperbaiki.ref
s, array, dll) yang menunjuk ke suatu struktur. Tetapi nilainya adalah referensi itu sendiri, bukan hal yang ditunjukkannya. Jika Anda memilikivar a = ref 1
dan membuat salinanvar b = a
, dan Anda menggunakanb
, apakah itu berarti Anda masih menggunakana
? Tidak. Anda memiliki akses ke struktur yang sama dengan yang ditunjukkan oleha
? Ya. Itulah cara kerja jenis ini di SML dan tidak ada hubungannya dengan penutupanPengumpulan sampah tidak diperlukan untuk menerapkan penutupan. Pada 2008, bahasa Delphi, yang bukan sampah yang dikumpulkan, menambahkan implementasi penutupan. Ini berfungsi seperti ini:
Kompiler membuat objek functor di bawah kap yang mengimplementasikan Antarmuka mewakili penutupan. Semua variabel lokal tertutup dapat diubah dari lokal untuk prosedur melampirkan ke bidang pada objek functor. Ini memastikan bahwa negara dipertahankan selama fungsi tersebut.
Batasan pada sistem ini adalah bahwa setiap parameter yang dilewatkan dengan mengacu pada fungsi penutup, serta nilai hasil fungsi, tidak dapat ditangkap oleh functor karena mereka bukan penduduk lokal yang cakupannya terbatas pada fungsi tutup.
Functor disebut oleh referensi penutupan, menggunakan gula sintaksis untuk membuatnya terlihat ke pengembang seperti penunjuk fungsi bukan Antarmuka. Ia menggunakan sistem penghitungan referensi Delphi untuk antarmuka untuk memastikan bahwa objek functor (dan semua status yang dipegangnya) tetap "hidup" selama diperlukan, dan kemudian dibebaskan ketika refcount turun ke 0.
sumber
shared_ptr
bersifat non-deterministik karena destruktor berpacu ke decrement ke nol.