1. ホーム
  2. javascript

[解決済み] 継続とコールバックの違いは何ですか?

2022-04-29 01:35:16

質問

継続についての知識を得るためにウェブを見て回っていますが、最も簡単な説明が、私のようなJavaScriptプログラマを全く困惑させるというのは、呆れるばかりです。特に、Schemeのコードやモナドを使って連続性を説明している記事が多いので、そう思います。

ようやく連続性の本質を理解できたと思ったところで、自分が知っていることが本当に真実なのかどうかを知りたくなった。もし、私が真実だと思っていることが、実は真実でないとしたら、それは無知であり、悟りではない。

そこで、私が知っていることを紹介します。

ほとんどすべての言語において、関数は呼び出し側に明示的に値(と制御)を返します。例えば

var sum = add(2, 3);

console.log(sum);

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

第一級関数を持つ言語では、呼び出し元に明示的に返す代わりに、コントロールと戻り値をコールバックに渡すことができる。

add(2, 3, function (sum) {
    console.log(sum);
});

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

このように、ある関数から値を返す代わりに、別の関数を続行させているのです。したがって、この関数は最初の関数の継続と呼ばれます。

では、コンティニュアスとコールバックの違いは何でしょうか?

どのように解決するのですか?

私は、継続はコールバックの特殊なケースであると考えています。ある関数は、任意の数の関数を、任意の回数、コールバックすることができます。例えば

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);
}

しかし、ある関数が最後に別の関数を呼び出す場合、その2番目の関数は最初の関数の継続と呼ばれます。例えば

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);
        }
    }
}

ある関数が最後に別の関数を呼び出すことをテールコールという。Schemeのようにテールコールを最適化する言語もある。つまり、テールコールは関数呼び出しのようなオーバーヘッドを完全に発生させないということだ。その代わり、単純なgotoとして実装される(呼び出し元の関数のスタックフレームはテールコールのスタックフレームに置き換えられる)。

ボーナス : 継続渡しスタイルに進む。次のようなプログラムを考えてみましょう。

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

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

さて、もしすべての演算(足し算、掛け算など)を関数の形で書いたとしたら、次のようになる。

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;
}

さらに、もし値を返すことが許されないのであれば、以下のようにコンティニュエーションを使用しなければならない。

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);
}

このように、値を返すことが許されない(したがって、継続を渡すことに頼らざるを得ない)プログラミングのスタイルを、継続渡しスタイルと呼びます。

しかし、継続渡しスタイルには2つの問題がある。

  1. 継続を渡すと、コールスタックのサイズが大きくなる。Schemeのように末尾の呼び出しを排除した言語でない限り、スタック容量が不足する危険性がある。
  2. ネストした関数を書くのが面倒。

最初の問題は、JavaScriptでは継続を非同期で呼び出すことで簡単に解決できる。継続を非同期に呼び出すことで、継続が呼び出される前に関数が返されます。そのため、コールスタックのサイズが大きくなることはありません。

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);
}

2つ目の問題は、通常、次のような関数を使用して解決されます。 call-with-current-continuation と略されることが多いのですが callcc . 残念ながら callcc はJavaScriptで完全に実装することはできませんが、ほとんどの使用例について代替関数を書くことができます。

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;
}

callcc 関数は関数 f に適用し、それを current-continuation (と略称される)。 cc ). その current-continuation を呼び出した後に、関数本体の残りの部分をラップする継続関数です。 callcc .

関数の本体を考える pythagoras :

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

current-continuation の2番目の callcc があります。

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

同様に current-continuation の最初の callcc があります。

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

というのは current-continuation の最初の callcc には、別の callcc の場合は、継続渡しスタイルに変換する必要があります。

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

そのため、基本的には callcc は、関数本体全体を元の状態に論理的に変換します (そして、これらの無名関数には cc ). このcallccの実装を使用したpythagoras関数は、次のようになります。

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

ここでも callcc はJavaScriptで実装できますが、以下のようにJavaScriptで継続渡しスタイルで実装します。

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);
}

機能 callcc は、try-catch ブロック、コルーチン、ジェネレータなどの複雑な制御フロー構造を実装するために使用することができます。 ファイバー など。