Saya telah menjelajahi seluruh web untuk mencari pencerahan tentang kelanjutan, dan sangat membingungkan bagaimana penjelasan yang paling sederhana dapat sangat membingungkan seorang programmer JavaScript seperti saya. Ini terutama benar ketika sebagian besar artikel menjelaskan kelanjutan dengan kode dalam Skema atau menggunakan monads.
Sekarang saya akhirnya berpikir saya telah memahami esensi kelanjutan, saya ingin tahu apakah yang saya tahu sebenarnya adalah kebenaran. Jika apa yang saya anggap benar tidak benar, maka itu adalah ketidaktahuan dan bukan pencerahan.
Jadi, inilah yang saya tahu:
Di hampir semua bahasa, fungsi secara eksplisit mengembalikan nilai (dan kontrol) ke pemanggil mereka. Sebagai contoh:
var sum = add(2, 3);
console.log(sum);
function add(x, y) {
return x + y;
}
Sekarang dalam bahasa dengan fungsi kelas satu kami dapat meneruskan kontrol dan mengembalikan nilai ke panggilan balik alih-alih secara eksplisit kembali ke pemanggil:
add(2, 3, function (sum) {
console.log(sum);
});
function add(x, y, cont) {
cont(x + y);
}
Jadi, alih-alih mengembalikan nilai dari fungsi, kami melanjutkan dengan fungsi lain. Karena itu fungsi ini disebut sebagai kelanjutan dari yang pertama.
Jadi apa perbedaan antara kelanjutan dan panggilan balik?
sumber
Jawaban:
Saya percaya bahwa kelanjutan adalah kasus khusus dari callback. Suatu fungsi dapat memanggil balik sejumlah fungsi, berapapun kali. Sebagai contoh:
Namun jika suatu fungsi memanggil kembali fungsi lain sebagai hal terakhir yang dilakukannya maka fungsi kedua disebut sebagai kelanjutan dari yang pertama. Sebagai contoh:
Jika suatu fungsi memanggil fungsi lain sebagai hal terakhir yang dilakukannya maka itu disebut panggilan ekor. Beberapa bahasa seperti Skema melakukan optimasi panggilan ekor. Ini berarti bahwa panggilan ekor tidak menimbulkan overhead penuh dari panggilan fungsi. Alih-alih itu diterapkan sebagai goto sederhana (dengan bingkai tumpukan fungsi panggilan digantikan oleh bingkai tumpukan panggilan ekor).
Bonus : Melanjutkan ke gaya kelanjutan passing. Pertimbangkan program berikut:
Sekarang jika setiap operasi (termasuk penambahan, perkalian, dll.) Ditulis dalam bentuk fungsi maka kita akan memiliki:
Selain itu jika kami tidak diizinkan untuk mengembalikan nilai apa pun maka kami harus menggunakan kelanjutan sebagai berikut:
Gaya pemrograman di mana Anda tidak diizinkan untuk mengembalikan nilai (dan karenanya Anda harus menggunakan kelanjutan kelanjutan) disebut gaya kelanjutan kelanjutan.
Namun ada dua masalah dengan gaya passing lanjutan:
Masalah pertama dapat dengan mudah diselesaikan dalam JavaScript dengan memanggil melanjutkan secara tidak sinkron. Dengan memanggil kelanjutan secara asinkron, fungsi kembali sebelum kelanjutan dipanggil. Karenanya ukuran tumpukan panggilan tidak bertambah:
Masalah kedua biasanya diselesaikan dengan menggunakan fungsi
call-with-current-continuation
yang sering disingkatcallcc
. Sayangnyacallcc
tidak dapat sepenuhnya diimplementasikan dalam JavaScript, tetapi kami dapat menulis fungsi penggantian untuk sebagian besar kasus penggunaannya:The
callcc
Fungsi mengambil fungsif
dan berlaku kecurrent-continuation
(disingkatcc
). Inicurrent-continuation
adalah fungsi kelanjutan yang membungkus seluruh tubuh fungsi setelah panggilan kecallcc
.Pertimbangkan tubuh fungsi
pythagoras
:Yang
current-continuation
keduacallcc
adalah:Demikian pula yang
current-continuation
pertamacallcc
adalah:Karena yang
current-continuation
pertamacallcc
berisi yang lain,callcc
itu harus dikonversi ke gaya kelanjutan passing:Jadi intinya
callcc
secara logis mengkonversi seluruh fungsi tubuh kembali ke apa yang kita mulai dari (dan memberikan fungsi-fungsi anonim namacc
). Fungsi pythagoras menggunakan implementasi callcc ini menjadi:Sekali lagi Anda tidak dapat menerapkan
callcc
dalam JavaScript, tetapi Anda dapat menerapkannya gaya meneruskan kelanjutan dalam JavaScript sebagai berikut:Fungsi
callcc
ini dapat digunakan untuk mengimplementasikan struktur aliran kontrol yang kompleks seperti blok uji coba, coroutine, generator, serat , dll.sumber
Meskipun ada tulisan yang bagus, saya pikir Anda sedikit membingungkan terminologi Anda. Misalnya, Anda benar bahwa panggilan ekor terjadi ketika panggilan adalah hal terakhir yang harus dijalankan oleh suatu fungsi, tetapi dalam kaitannya dengan kelanjutan, panggilan ekor berarti fungsi tersebut tidak mengubah kelanjutan yang dipanggil, hanya bahwa memperbarui nilai yang diteruskan ke kelanjutan (jika diinginkan). Inilah sebabnya mengapa mengkonversi fungsi rekursif ekor ke CPS sangat mudah (Anda hanya menambahkan kelanjutan sebagai parameter dan memanggil kelanjutan pada hasilnya).
Ini juga agak aneh untuk memanggil kelanjutan kasus khusus dari callback. Saya bisa melihat bagaimana mereka mudah dikelompokkan bersama, tetapi kelanjutan tidak muncul dari kebutuhan untuk membedakan dari panggilan balik. Kelanjutan sebenarnya merupakan petunjuk yang tersisa untuk menyelesaikan perhitungan , atau sisa perhitungan dari ini titik waktu. Anda bisa menganggap kelanjutan sebagai lubang yang perlu diisi. Jika saya bisa menangkap kelanjutan program saat ini, maka saya bisa kembali ke persis bagaimana program itu ketika saya menangkap kelanjutan. (Itu pasti membuat debugger lebih mudah untuk menulis.)
Dalam konteks ini, jawaban untuk pertanyaan Anda adalah bahwa panggilan balik adalah hal umum yang dipanggil pada titik waktu tertentu yang ditentukan oleh beberapa kontrak yang disediakan oleh penelepon [panggilan balik]. Panggilan balik dapat memiliki argumen sebanyak yang diinginkan dan disusun dengan cara apa pun yang diinginkan. Sebuah kelanjutan , kemudian, adalah tentu prosedur satu argumen yang memecahkan nilai yang dikirimkan ke dalamnya. Kelanjutan harus diterapkan ke nilai tunggal dan aplikasi harus terjadi di akhir. Ketika kelanjutan selesai mengeksekusi ekspresi selesai, dan, tergantung pada semantik bahasa, efek samping mungkin atau mungkin tidak dihasilkan.
sumber
Jawaban singkatnya adalah bahwa perbedaan antara kelanjutan dan panggilan balik adalah bahwa setelah panggilan balik dipanggil (dan telah selesai) eksekusi dilanjutkan pada titik itu dipanggil, sementara memohon kelanjutan menyebabkan eksekusi untuk melanjutkan pada titik kelanjutan dibuat. Dengan kata lain: kelanjutan tidak pernah kembali .
Pertimbangkan fungsinya:
(Saya menggunakan sintaks Javascript meskipun Javascript sebenarnya tidak mendukung kelanjutan kelas satu karena ini adalah contoh yang Anda berikan, dan itu akan lebih mudah dipahami oleh orang yang tidak terbiasa dengan sintaks Lisp.)
Sekarang, jika kita berikan panggilan balik:
maka kita akan melihat tiga peringatan: "sebelum", "5" dan "setelah".
Di sisi lain, jika kita meneruskannya kelanjutan yang melakukan hal yang sama seperti panggilan balik, seperti ini:
maka kita akan melihat hanya dua peringatan: "sebelum" dan "5". Memohon ke
c()
dalamadd()
mengakhiri eksekusiadd()
dan menyebabkancallcc()
untuk kembali; nilai yang dikembalikancallcc()
adalah nilai yang diteruskan sebagai argumen untukc
(yaitu, jumlah).Dalam pengertian ini, meskipun memohon kelanjutan tampak seperti pemanggilan fungsi, itu dalam beberapa hal lebih mirip dengan pernyataan pengembalian atau melemparkan pengecualian.
Bahkan, panggilan / cc dapat digunakan untuk menambahkan pernyataan kembali ke bahasa yang tidak mendukungnya. Misalnya, jika JavaScript tidak memiliki pernyataan pengembalian (sebagai gantinya, seperti banyak bahasa Bibir, hanya mengembalikan nilai ekspresi terakhir di badan fungsi) tetapi memang memiliki panggilan / cc, kami dapat menerapkan pengembalian seperti ini:
Memanggil
return(i)
memanggil lanjutan yang menghentikan eksekusi fungsi anonim dan menyebabkancallcc()
untuk mengembalikan indeksi
yangtarget
ditemukan dimyArray
.(NB: ada beberapa cara analogi "pengembalian" sedikit disederhanakan. Misalnya, jika kelanjutan lolos dari fungsi yang dibuatnya - dengan diselamatkan di tempat global, katakanlah - ada kemungkinan fungsi tersebut yang membuat kelanjutan dapat kembali beberapa kali meskipun itu hanya dipanggil sekali .)
Panggilan / cc juga dapat digunakan untuk menerapkan penanganan pengecualian (melempar dan mencoba / menangkap), loop, dan banyak struktur contol lainnya.
Untuk menghapus beberapa kemungkinan kesalahpahaman:
Optimasi panggilan ekor tidak dengan cara apa pun diperlukan untuk mendukung kelanjutan kelas satu. Pertimbangkan bahwa bahkan bahasa C memiliki bentuk kelanjutan (terbatas) dalam bentuk
setjmp()
, yang menciptakan kelanjutan, danlongjmp()
, yang memanggilnya!Tidak ada alasan khusus kelanjutan hanya perlu satu argumen. Hanya saja argumen untuk kelanjutan menjadi nilai kembali panggilan / cc, dan panggilan / cc biasanya didefinisikan sebagai memiliki nilai pengembalian tunggal, jadi tentu saja kelanjutan harus mengambil tepat satu. Dalam bahasa dengan dukungan untuk beberapa nilai pengembalian (seperti Common Lisp, Go, atau bahkan Skema), sangat mungkin memiliki kelanjutan yang menerima beberapa nilai.
sumber