Apa perbedaan antara kelanjutan dan panggilan balik?

133

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?

Aadit M Shah
sumber
4
Sebagian dari saya berpikir ini adalah pertanyaan yang sangat bagus dan sebagian dari saya berpikir itu terlalu panjang dan mungkin hanya menghasilkan jawaban 'ya / tidak'. Namun karena upaya dan penelitian yang terlibat saya pergi dengan perasaan pertama saya.
Andras Zoltan
2
Apa pertanyaanmu? Kedengarannya Anda mengerti ini dengan cukup baik.
Michael Aaron Safyan
3
Ya saya setuju - saya pikir itu seharusnya posting blog lebih lanjut di sepanjang baris 'Kelanjutan JavaScript - apa yang saya pahami sebagai'.
Andras Zoltan
9
Nah, ada pertanyaan penting: "Jadi apa perbedaan antara kelanjutan dan panggilan balik?", Diikuti oleh "Saya percaya ...". Jawaban atas pertanyaan itu mungkin menarik?
Kebingungan
3
Sepertinya ini mungkin lebih tepat diposting di programmers.stackexchange.com.
Brian Reischl

Jawaban:

164

Saya percaya bahwa kelanjutan adalah kasus khusus dari callback. Suatu fungsi dapat memanggil balik sejumlah fungsi, berapapun kali. Sebagai contoh:

var array = [1, 2, 3];

forEach(array, function (element, array, index) {
    array[index] = 2 * element;
});

console.log(array);

function forEach(array, callback) {
    var length = array.length;
    for (var i = 0; i < length; i++)
        callback(array[i], array, i);
}

Namun jika suatu fungsi memanggil kembali fungsi lain sebagai hal terakhir yang dilakukannya maka fungsi kedua disebut sebagai kelanjutan dari yang pertama. Sebagai contoh:

var array = [1, 2, 3];

forEach(array, function (element, array, index) {
    array[index] = 2 * element;
});

console.log(array);

function forEach(array, callback) {
    var length = array.length;

    // This is the last thing forEach does
    // cont is a continuation of forEach
    cont(0);

    function cont(index) {
        if (index < length) {
            callback(array[index], array, index);
            // This is the last thing cont does
            // cont is a continuation of itself
            cont(++index);
        }
    }
}

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:

console.log(pythagoras(3, 4));

function pythagoras(x, y) {
    return x * x + y * y;
}

Sekarang jika setiap operasi (termasuk penambahan, perkalian, dll.) Ditulis dalam bentuk fungsi maka kita akan memiliki:

console.log(pythagoras(3, 4));

function pythagoras(x, y) {
    return add(square(x), square(y));
}

function square(x) {
    return multiply(x, x);
}

function multiply(x, y) {
    return x * y;
}

function add(x, y) {
    return x + y;
}

Selain itu jika kami tidak diizinkan untuk mengembalikan nilai apa pun maka kami harus menggunakan kelanjutan sebagai berikut:

pythagoras(3, 4, console.log);

function pythagoras(x, y, cont) {
    square(x, function (x_squared) {
        square(y, function (y_squared) {
            add(x_squared, y_squared, cont);
        });
    });
}

function square(x, cont) {
    multiply(x, x, cont);
}

function multiply(x, y, cont) {
    cont(x * y);
}

function add(x, y, cont) {
    cont(x + y);
}

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:

  1. Melewati kelanjutan meningkatkan ukuran tumpukan panggilan. Kecuali jika Anda menggunakan bahasa seperti Skema yang menghilangkan panggilan ekor, Anda akan berisiko kehabisan ruang stack.
  2. Sungguh menyakitkan menulis fungsi bersarang.

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:

Function.prototype.async = async;

pythagoras.async(3, 4, console.log);

function pythagoras(x, y, cont) {
    square.async(x, function (x_squared) {
        square.async(y, function (y_squared) {
            add.async(x_squared, y_squared, cont);
        });
    });
}

function square(x, cont) {
    multiply.async(x, x, cont);
}

function multiply(x, y, cont) {
    cont.async(x * y);
}

function add(x, y, cont) {
    cont.async(x + y);
}

function async() {
    setTimeout.bind(null, this, 0).apply(null, arguments);
}

Masalah kedua biasanya diselesaikan dengan menggunakan fungsi call-with-current-continuationyang sering disingkat callcc. Sayangnya callcctidak dapat sepenuhnya diimplementasikan dalam JavaScript, tetapi kami dapat menulis fungsi penggantian untuk sebagian besar kasus penggunaannya:

pythagoras(3, 4, console.log);

function pythagoras(x, y, cont) {
    var x_squared = callcc(square.bind(null, x));
    var y_squared = callcc(square.bind(null, y));
    add(x_squared, y_squared, cont);
}

function square(x, cont) {
    multiply(x, x, cont);
}

function multiply(x, y, cont) {
    cont(x * y);
}

function add(x, y, cont) {
    cont(x + y);
}

function callcc(f) {
    var cc = function (x) {
        cc = x;
    };

    f(cc);

    return cc;
}

The callccFungsi mengambil fungsi fdan berlaku ke current-continuation(disingkat cc). Ini current-continuationadalah fungsi kelanjutan yang membungkus seluruh tubuh fungsi setelah panggilan ke callcc.

Pertimbangkan tubuh fungsi pythagoras:

var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);

Yang current-continuationkedua callccadalah:

function cc(y_squared) {
    add(x_squared, y_squared, cont);
}

Demikian pula yang current-continuationpertama callccadalah:

function cc(x_squared) {
    var y_squared = callcc(square.bind(null, y));
    add(x_squared, y_squared, cont);
}

Karena yang current-continuationpertama callccberisi yang lain, callccitu harus dikonversi ke gaya kelanjutan passing:

function cc(x_squared) {
    square(y, function cc(y_squared) {
        add(x_squared, y_squared, cont);
    });
}

Jadi intinya callccsecara logis mengkonversi seluruh fungsi tubuh kembali ke apa yang kita mulai dari (dan memberikan fungsi-fungsi anonim nama cc). Fungsi pythagoras menggunakan implementasi callcc ini menjadi:

function pythagoras(x, y, cont) {
    callcc(function(cc) {
        square(x, function (x_squared) {
            square(y, function (y_squared) {
                add(x_squared, y_squared, cont);
            });
        });
    });
}

Sekali lagi Anda tidak dapat menerapkan callccdalam JavaScript, tetapi Anda dapat menerapkannya gaya meneruskan kelanjutan dalam JavaScript sebagai berikut:

Function.prototype.async = async;

pythagoras.async(3, 4, console.log);

function pythagoras(x, y, cont) {
    callcc.async(square.bind(null, x), function cc(x_squared) {
        callcc.async(square.bind(null, y), function cc(y_squared) {
            add.async(x_squared, y_squared, cont);
        });
    });
}

function square(x, cont) {
    multiply.async(x, x, cont);
}

function multiply(x, y, cont) {
    cont.async(x * y);
}

function add(x, y, cont) {
    cont.async(x + y);
}

function async() {
    setTimeout.bind(null, this, 0).apply(null, arguments);
}

function callcc(f, cc) {
    f.async(cc);
}

Fungsi callccini dapat digunakan untuk mengimplementasikan struktur aliran kontrol yang kompleks seperti blok uji coba, coroutine, generator, serat , dll.

Aadit M Shah
sumber
10
Saya sangat berterima kasih kata-kata tidak bisa menggambarkan. Saya akhirnya mengerti pada intuisi semua konsep yang berhubungan dengan kelanjutan dalam satu sapuan! Saya baru setelah diklik, itu akan menjadi sederhana dan saya akan melihat saya menggunakan pola berkali-kali sebelumnya tanpa sadar, dan itu hanya seperti itu. Terima kasih banyak atas penjelasan yang indah dan jelas.
ata
2
Trampolin adalah hal yang cukup sederhana, namun kuat. Silakan periksa posting Reginald Braithwaite tentang mereka.
Marco Faustinelli
1
Terima kasih atas jawabannya. Saya ingin tahu apakah Anda dapat memberikan lebih banyak dukungan untuk pernyataan bahwa callcc tidak dapat diimplementasikan dalam JavaScript? Mungkin penjelasan tentang apa yang dibutuhkan JavaScript untuk mengimplementasikannya?
John Henry
1
@JohnHenry - well, sebenarnya ada implementasi panggilan / cc dalam JavaScript yang dilakukan oleh Matt Might ( matt.might.net/articles/by-example-continuation-passing-style - pergi ke paragraf terakhir), tapi tolong jangan ' t tanya saya bagaimana cara kerjanya atau bagaimana menggunakannya :-)
Marco Faustinelli
1
@JohnHenry JS akan membutuhkan kelanjutan kelas satu (anggap saja sebagai mekanisme untuk menangkap status tertentu dari tumpukan panggilan). Tetapi hanya memiliki fungsi dan penutupan Kelas Satu, sehingga CPS adalah satu-satunya cara untuk meniru kelanjutan. Dalam skema, cont adalah implisit dan bagian dari tugas callcc adalah untuk "memverifikasi" conts implisit ini, sehingga fungsi konsumsi memiliki akses kepada mereka. Itu sebabnya callcc dalam Skema mengharapkan fungsi sebagai satu-satunya argumen. Versi CPS dari callcc di JS berbeda, karena cont dilewatkan sebagai argumen func eksplisit. Jadi callcc Aadit sudah cukup untuk banyak aplikasi.
scriptum
27

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.

Dcow
sumber
3
Terimakasih atas klarifikasi Anda. Kamu benar. Kelanjutan sebenarnya adalah reifikasi dari status kontrol program: potret keadaan program pada titik waktu tertentu. Fakta bahwa itu dapat disebut seperti fungsi normal tidak relevan. Kelanjutan sebenarnya bukan fungsi. Sebaliknya panggilan balik sebenarnya adalah fungsi. Itulah perbedaan nyata antara kelanjutan dan panggilan balik. Namun demikian JS tidak mendukung kelanjutan kelas satu. Hanya fungsi kelas satu. Oleh karena itu kelanjutan yang ditulis dalam CPS di JS hanyalah fungsi. Terima kasih atas masukannya. =)
Aadit M Shah
4
@ AditMShah ya, saya salah bicara di sana. Kelanjutan tidak harus berupa fungsi (atau prosedur seperti yang saya sebutkan). Menurut definisi, ini hanyalah representasi abstrak dari hal-hal yang akan datang. Namun, bahkan dalam Skema kelanjutan dipanggil seperti prosedur dan diedarkan sebagai satu. Hmm .. ini memunculkan pertanyaan yang sama menariknya tentang kelanjutan seperti apa yang bukan fungsi / prosedur.
dcow
@AaditMShah cukup menarik sehingga saya melanjutkan diskusi di sini: programmers.stackexchange.com/questions/212057/…
dcow
14

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:

function add(x, y, c) {
    alert("before");
    c(x+y);
    alert("after");
}

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

add(2, 3, function (sum) {
    alert(sum);
});

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:

alert(callcc(function(cc) {
    add(2, 3, cc);
}));

maka kita akan melihat hanya dua peringatan: "sebelum" dan "5". Memohon ke c()dalam add()mengakhiri eksekusi add()dan menyebabkan callcc()untuk kembali; nilai yang dikembalikan callcc()adalah nilai yang diteruskan sebagai argumen untuk c(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:

function find(myArray, target) {
    callcc(function(return) {
        var i;
        for (i = 0; i < myArray.length; i += 1) {
            if(myArray[i] === target) {
                return(i);
            }
        }
        return(undefined); // Not found.
    });
}

Memanggil return(i)memanggil lanjutan yang menghentikan eksekusi fungsi anonim dan menyebabkan callcc()untuk mengembalikan indeks iyang targetditemukan di myArray.

(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, dan longjmp(), yang memanggilnya!

    • Di sisi lain, jika Anda secara naif mencoba untuk menulis program Anda dengan gaya kelanjutan yang lewat tanpa optimisasi panggilan ekor, Anda pada akhirnya akan ditumpuk dengan berlebihan.
  • 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.

callallen
sumber
2
Maaf jika saya membuat kesalahan dalam contoh JavaScript. Menulis jawaban ini secara kasar menggandakan jumlah total JavaScript yang saya tulis.
cpcallen
Apakah saya mengerti benar bahwa Anda berbicara tentang kelanjutan yang tidak didahulukan dalam jawaban ini, dan jawaban yang diterima berbicara tentang kelanjutan yang dibatasi?
Jozef Mikušinec
1
"Meminta kelanjutan menyebabkan eksekusi berlanjut pada titik kelanjutan dibuat" - saya pikir Anda bingung "menciptakan" kelanjutan dengan menangkap kelanjutan saat ini .
Alexey
@ Alexey: ini adalah jenis kesedihan yang saya setujui. Tetapi sebagian besar bahasa tidak menyediakan cara apa pun untuk membuat kelanjutan (reified) selain dengan menangkap kelanjutan saat ini.
cpcallen
1
@ Jozef: Saya tentu saja berbicara tentang kelanjutan yang tidak didahului. Saya pikir itu adalah niat Aadit juga, meskipun ketika dcow mencatat jawaban yang diterima gagal untuk membedakan kelanjutan dari panggilan ekor (terkait erat), dan saya perhatikan bahwa kelanjutan dibatasi dibatasi setara dengan fungsi / prosedur: community.schemewiki.org/ ? composable-continuations-tutorial
cpcallen