Apakah pengumpulan sampah diperlukan untuk menerapkan penutupan yang aman?

14

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_from1Fungsi menggunakan penutupan penolong countyang menangkap dan menggunakan variabel xdari 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 mterikat 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.

Giorgio
sumber
2
Setiap variabel yang ditutup harus dilindungi dari pengumpulan sampah, dan variabel apa pun yang tidak ditutup harus memenuhi syarat untuk pengumpulan sampah. Oleh karena itu setiap mekanisme yang dapat secara andal melacak apakah suatu variabel ditutup atau tidak juga dapat dengan andal merebut kembali memori yang ditempati variabel tersebut.
Robert Harvey
3
@ btilly: Penghitungan ulang hanyalah salah satu dari banyak strategi implementasi yang berbeda untuk pemulung. Tidak masalah bagaimana penerapan GC untuk tujuan pertanyaan ini.
Jörg W Mittag
3
@ btilly: Apa arti pengumpulan sampah "benar"? Penghitungan ulang hanyalah cara lain untuk menerapkan GC. Menelusuri lebih populer, mungkin karena sulitnya mengumpulkan siklus dengan penghitungan ulang. (Biasanya, Anda berakhir dengan pelacakan GC terpisah, jadi mengapa repot-repot menerapkan dua GC jika Anda bisa bertahan dengan satu.) Tetapi ada cara lain untuk berurusan dengan siklus. 1) Hanya melarang mereka. 2) Abaikan saja. (Jika Anda melakukan implementasi untuk skrip satu kali saja, mengapa tidak?) 3) Cobalah untuk mendeteksinya secara eksplisit. (Ternyata memiliki refcount yang tersedia dapat mempercepatnya.)
Jörg W Mittag
1
Itu tergantung pada mengapa Anda ingin penutupan di tempat pertama. Jika Anda ingin menerapkan, katakanlah, semantik kalkulus lambda penuh, Anda pasti membutuhkan GC, titik. Tidak ada jalan lain. Jika Anda menginginkan sesuatu yang sangat mirip dengan penutupan, tetapi tidak mengikuti semantik yang tepat seperti itu (seperti dalam C ++, Delphi, apa pun) - lakukan apa pun yang Anda inginkan, gunakan analisis wilayah, gunakan manajemen memori sepenuhnya manual.
SK-logic
2
@Mason Wheeler: Penutupan hanyalah nilai, secara umum tidak mungkin untuk memprediksi bagaimana mereka akan dipindahkan saat runtime. Dalam hal ini, mereka tidak ada yang istimewa, hal yang sama akan berlaku untuk string, daftar, dan sebagainya.
Giorgio

Jawaban:

14

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.

barjak
sumber
1
Tautan dan referensi yang sangat menarik untuk bahasa Rust. Terima kasih. +1.
Giorgio
1
Saya banyak berpikir sebelum menerima jawaban karena saya menemukan jawaban Mason juga sangat informatif. Saya memilih yang ini karena informatif dan mengutip bahasa yang kurang dikenal dengan pendekatan penutupan asli.
Giorgio
Terima kasih untuk itu. Saya sangat antusias dengan bahasa muda ini, dan saya senang berbagi minat saya. Saya tidak tahu apakah penutupan yang aman dimungkinkan tanpa GC, sebelum saya mendengar tentang Rust.
barjak
9

Sayangnya dimulai dengan GC membuat Anda menjadi korban sindrom XY:

  • penutupan membutuhkan dari variabel yang mereka tutupi hidup selama penutupan itu (untuk alasan keamanan)
  • menggunakan GC kita bisa memperpanjang masa pakai variabel-variabel itu cukup lama
  • Sindrom XY: apakah ada mekanisme lain untuk memperpanjang umur?

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):

  1. Perpanjang masa berlaku variabel tertutup (seperti halnya GC, misalnya)
  2. Batasi umur penutupan

Yang terakhir hanyalah pendekatan simetris. Ini tidak sering digunakan, tetapi jika, seperti Rust, Anda memiliki sistem tipe sadar wilayah, maka itu pasti mungkin.

Matthieu M.
sumber
7

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 __blockvariabel 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.

pengguna102008
sumber
4
"Pengumpulan sampah tidak diperlukan untuk penutupan.": Pertanyaannya adalah apakah itu diperlukan agar bahasa dapat memberlakukan penutupan yang aman. Saya tahu bahwa saya bisa menulis penutupan yang aman di C ++ tetapi bahasa tidak mendukungnya. Untuk penutupan yang memperpanjang masa pakai variabel yang diambil, lihat hasil edit pertanyaan saya.
Giorgio
1
Saya kira pertanyaannya bisa diubah menjadi: untuk penutupan yang aman .
Matthieu M.
1
Judul berisi istilah "penutupan aman", apakah Anda pikir saya bisa merumuskannya dengan cara yang lebih baik?
Giorgio
1
Bisakah Anda memperbaiki paragraf kedua? Di SML, penutupan memang memperpanjang masa pakai data yang dirujuk oleh variabel yang ditangkap. Juga, memang benar bahwa Anda tidak dapat menetapkan variabel (mengubah pengikatannya) tetapi Anda memang memiliki data yang dapat diubah (melalui ref). Jadi, oke, orang bisa memperdebatkan apakah pelaksanaan penutupan terkait dengan pengumpulan sampah atau tidak, tetapi pernyataan di atas harus diperbaiki.
Giorgio
1
@Iorgio: Bagaimana kalau sekarang? Juga, dalam hal apa Anda menemukan pernyataan saya bahwa penutupan tidak perlu memperpanjang masa hidup variabel yang ditangkap salah? Ketika Anda berbicara tentang data yang bisa berubah, Anda berbicara tentang tipe referensi ( refs, array, dll) yang menunjuk ke suatu struktur. Tetapi nilainya adalah referensi itu sendiri, bukan hal yang ditunjukkannya. Jika Anda memiliki var a = ref 1dan membuat salinan var b = a, dan Anda menggunakan b, apakah itu berarti Anda masih menggunakan a? Tidak. Anda memiliki akses ke struktur yang sama dengan yang ditunjukkan oleh a? Ya. Itulah cara kerja jenis ini di SML dan tidak ada hubungannya dengan penutupan
user102008
6

Pengumpulan 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.

Mason Wheeler
sumber
1
Ah, jadi itu hanya mungkin untuk menangkap variabel lokal, bukan argumen! Ini sepertinya pertukaran yang masuk akal dan pintar! +1
Giorgio
1
@Iorgio: Itu bisa menangkap argumen, hanya saja bukan yang merupakan parameter var .
Mason Wheeler
2
Anda juga kehilangan kemampuan untuk memiliki 2 penutupan yang berkomunikasi melalui negara pribadi bersama. Anda tidak akan menemukan itu dalam kasus penggunaan dasar, tetapi membatasi kemampuan Anda untuk melakukan hal-hal yang kompleks. Masih contoh yang bagus tentang apa yang mungkin!
btilly
3
@ btilly: Sebenarnya, jika Anda menempatkan 2 penutupan di dalam fungsi penutup yang sama, itu sah-sah saja. Mereka akhirnya berbagi objek functor yang sama, dan jika mereka memodifikasi keadaan yang sama satu sama lain, perubahan di satu akan tercermin di yang lain.
Mason Wheeler
2
@MasonWheeler: "Tidak. Pengumpulan sampah bersifat non-deterministik; tidak ada jaminan bahwa objek tertentu akan dikumpulkan, apalagi ketika akan terjadi. Tetapi penghitungan referensi bersifat deterministik: Anda dijamin oleh kompiler bahwa objek tersebut akan dibebaskan segera setelah hitungan turun menjadi 0. ". Jika saya memiliki uang receh untuk setiap kali saya mendengar mitos itu diabadikan. OCaml memiliki GC deterministik. Safe thread C ++ shared_ptrbersifat non-deterministik karena destruktor berpacu ke decrement ke nol.
Jon Harrop