연속과 콜백의 차이점은 무엇입니까?
나는 연속체에 대한 깨달음을 찾기 위해 웹 전체를 탐색하고 있으며, 가장 간단한 설명이 나 자신과 같은 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);
}
그러나 함수가 마지막 함수로 다른 함수를 호출하면 두 번째 함수를 첫 번째 함수의 연속이라고합니다. 예를 들면 다음과 같습니다.
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과 같은 일부 언어는 테일 콜 최적화를 수행합니다. 이는 테일 호출이 함수 호출의 전체 오버 헤드를 발생시키지 않음을 의미합니다. 대신 간단한 호출로 구현됩니다 (호출 함수의 스택 프레임이 테일 호출의 스택 프레임으로 대체 됨).
보너스 : 연속 합격 스타일로 진행합니다. 다음 프로그램을 고려하십시오.
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);
}
값을 반환 할 수없는 연속 프로그래밍 스타일을 연속 전달 스타일이라고합니다.
그러나 연속 전달 스타일에는 두 가지 문제가 있습니다.
- 연속을 통과하면 호출 스택의 크기가 증가합니다. 테일 호출을 제거하는 Scheme과 같은 언어를 사용하지 않으면 스택 공간이 부족해질 위험이 있습니다.
- 중첩 함수를 작성하는 것은 고통입니다.
첫 번째 문제는 연속을 비동기 적으로 호출하여 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);
}
두 번째 문제는 일반적으로 call-with-current-continuation로 약칭되는 이라는 함수를 사용하여 해결 됩니다 callcc. 불행히도 callccJavaScript로 완전히 구현할 수는 없지만 대부분의 사용 사례에 대한 대체 함수를 작성할 수 있습니다.
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-continuationcccurrent-continuationcallcc
함수의 본문을 고려하십시오 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제의은 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);
});
}
So essentially callcc logically converts the entire function body back to what we started from (and gives those anonymous functions the name cc). The pythagoras function using this implementation of callcc becomes then:
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
Again you can't implement callcc in JavaScript, but you can implement it the continuation passing style in JavaScript as follows:
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);
}
The function callcc can be used to implement complex control flow structures such as try-catch blocks, coroutines, generators, fibers, etc.
Despite the wonderful writeup, I think you're confusing your terminology a bit. For example, you are correct that a tail call happens when the call is the last thing a function needs to execute, but in relation to continuations, a tail call means the function does not modify the continuation that it is called with, only that it updates the value passed to the continuation (if it desires). This is why converting a tail recursive function to CPS is so easy (you just add the continuation as a parameter and call the continuation on the result).
It's also a bit odd to call continuations a special case of callbacks. I can see how they are easily grouped together, but continuations didn't arise from the need to distinguish from a callback. A continuation actually represents the instructions remaining to complete a computation, or the remainder of the computation from this point in time. You can think of a continuation as a hole that needs to be filled in. If I can capture a program's current continuation, then I can go back to exactly how the program was when I captured the continuation. (That sure makes debuggers easier to write.)
In this context, the answer to your question is that a callback is a generic thing that gets called at any point in time specified by some contract provided by the caller [of the callback]. A callback can have as many arguments as it wants and be structured in any way it wants. A continuation, then, is necessarily a one argument procedure that resolves the value passed into it. A continuation must be applied to a single value and the application must happen at the end. When a continuation finishes executing the expression is complete, and, depending on the semantics of the language, side effects may or may not have been generated.
The short answer is that the difference between a continuation and a callback is that after a callback is invoked (and has finished) execution resumes at the point it was invoked, while invoking a continuation causes execution to resume at the point the continuation was created. In other words: a continuation never returns.
Consider the function:
function add(x, y, c) {
alert("before");
c(x+y);
alert("after");
}
(I use Javascript syntax even though Javascript doesn't actually support first-class continuations because this was what you gave your examples in, and it will be more comprehensible to people not familiar with Lisp syntax.)
Now, if we pass it a callback:
add(2, 3, function (sum) {
alert(sum);
});
then we will see three alerts: "before", "5" and "after".
On the other hand, if we were to pass it a continuation that does the same thing as the callback does, like this:
alert(callcc(function(cc) {
add(2, 3, cc);
}));
then we would see just two alerts: "before" and "5". Invoking c() inside add() ends the execution of add() and causes callcc() to return; the value returned by callcc() was the valued passed as the argument to c (namely, the sum).
In this sense, even though invoking a continuation looks like a function call, it is in some ways more akin to a return statement or throwing an exception.
In fact, call/cc can be used to add return statements to languages that don't support them. For example, if JavaScript didn't have return statement (instead, like many Lips languages, just returning the value of the last expression in the function body) but did have call/cc, we could implement return like this:
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.
});
}
Calling return(i) invokes a continuation that terminates the execution of the anonymous function and causes callcc() to return the index i at which target was found in myArray.
(N.B.: there are some ways in which the "return" analogy is a bit simplistic. For example, if a continuation escapes from the function it was created in - by being saved in a global somewhere, say - it is possible that the function that created the continuation can return multiple times even though it was only invoked once.)
Call/cc can similarly be used to implement exception handling (throw and try/catch), loops, and many other contol structures.
To clear up some possible misapprehensions:
Tail call optimisation is not by any means required in order to support first-class continuations. Consider that even the C language has a (restricted) form of continuations in the form of
setjmp(), which creates a continuation, andlongjmp(), which invokes one!- On the other hand, if you naively try to write your program in continuation passing style without tail call optimisation you are doomed to eventually overflow the stack.
There is no particular reason a continuation need take only one argument. It's just that argument(s) to the continuation become the return value(s) of call/cc, and call/cc is typically defined as having a single return value, so naturally the continuation must take exactly one. In languages with support for multiple return values (like Common Lisp, Go, or indeed Scheme) it would be entirely possible have continuations that accept multiple values.
'Programming' 카테고리의 다른 글
| Git Repo에서 기존 파일 제거 (0) | 2020.07.07 |
|---|---|
| Clang은 왜 x * 1.0을 최적화하지만 x + 0.0은 최적화하지 않습니까? (0) | 2020.07.07 |
| 배쉬 인 경우 [false]; (0) | 2020.07.07 |
| rsync : 서버에서 대상 디렉토리를 만들도록 어떻게 구성 할 수 있습니까? (0) | 2020.07.07 |
| virtualenv-사이트 패키지 없음 및 pip가 여전히 글로벌 패키지를 찾고 있습니까? (0) | 2020.07.07 |