연속과 콜백의 차이점은 무엇입니까?


133

나는 연속체에 대한 깨달음을 찾기 위해 웹 전체를 탐색하고 있으며, 가장 간단한 설명이 나 자신과 같은 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);
}

따라서 함수에서 값을 반환하는 대신 다른 함수를 계속 사용합니다. 따라서이 기능을 첫 번째 연속이라고합니다.

계속과 콜백의 차이점은 무엇입니까?


4
저의 일부는 이것이 정말로 좋은 질문이라고 생각하고 저의 일부는 그것이 너무 길다고 생각하며 아마도 '예 / 아니오'라고 답할 것입니다. 그러나 관련된 노력과 연구로 인해 나는 나의 첫 번째 느낌으로 가고 있습니다.
Andras Zoltan

2
질문이 뭐야? 당신이 이것을 잘 이해하는 것처럼 들립니다.
Michael Aaron Safyan

3
예, 동의합니다. 아마도 'JavaScript Continuations-내가 이해하는 것'의 행을 따라 더 많은 블로그 게시물이었던 것 같습니다.
Andras Zoltan

9
"계속과 콜백의 차이점은 무엇입니까?"라는 중요한 질문이 있습니다. 그 다음에 "믿습니다 ..."라는 질문이 있습니다. 그 질문에 대한 답이 흥미로울 수 있습니까?
혼란

3
이것은 programmers.stackexchange.com에 더 적절하게 게시 된 것처럼 보입니다.
Brian Reischl

답변:


164

나는 연속이 특별한 콜백 사례라고 생각합니다. 함수는 여러 함수를 여러 번 콜백 할 수 있습니다. 예를 들면 다음과 같습니다.

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

값을 반환 할 수없는 연속 프로그래밍 스타일을 연속 전달 스타일이라고합니다.

그러나 연속 전달 스타일에는 두 가지 문제가 있습니다.

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

두 번째 문제는 일반적으로 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);
    });
}

따라서 본질적 callcc으로 전체 함수 본문을 우리가 처음 시작한 것으로 다시 변환하고 익명 함수에 이름을 부여합니다 cc. 이 callcc 구현을 사용하는 피타고라스 함수는 다음과 같습니다.

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

다시 callccJavaScript로 구현할 수 없지만 다음과 같이 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 블록, 코 루틴, 발전기, 섬유 등과 같은 복잡한 제어 흐름 구조를 구현하는 데 사용할 수 있습니다 .


10
정말 감사 한 말로 설명 할 수 없습니다. 마침내 직관 수준에서 모든 연속 관련 개념을 한 번에 이해할 수있었습니다! 한 번 클릭하면 새롭고 간단 해졌으며 무의식적으로 패턴을 여러 번 사용했음을 알았습니다. 훌륭하고 명확한 설명에 감사드립니다.
ata

2
트램폴린은 상당히 단순하지만 강력한 것입니다. Reginald Braithwaite의 게시물을 확인하십시오 .
Marco Faustinelli

1
답변 해주셔서 감사합니다. callcc를 JavaScript로 구현할 수 없다는 문장에 대해 더 많은 지원을 제공 할 수 있을지 궁금합니다. JavaScript를 구현하는 데 필요한 JavaScript에 대한 설명입니까?
John Henry

1
@JohnHenry-실제로 Matt Might가 수행 한 JavaScript에는 call / cc 구현이 있습니다 ( matt.might.net/articles/by-example-continuation-passing-style- 마지막 단락으로 이동하십시오) 이 :-) 작동이나 사용 방법을 어떻게 t은 나 한테 물어
마르코 Faustinelli에게

1
@JohnHenry JS는 일급 연속이 필요합니다 (콜 스택의 특정 상태를 캡처하는 메커니즘으로 생각하십시오). 그러나 퍼스트 클래스 기능과 클로저 만 있으므로 CPS가 연속을 모방하는 유일한 방법입니다. 구성표에서 conts는 암시 적이며 callcc의 작업 중 일부는 이러한 암시 적 cont를 "정류"하여 소비하는 기능에 액세스 할 수 있도록하는 것입니다. 이것이 Scheme의 callcc가 유일한 인수로 함수를 기대하는 이유입니다. cont가 명시적인 func 인수로 전달되므로 JS에서 callcc의 CPS 버전이 다릅니다. 따라서 Aadit의 callcc는 많은 응용 프로그램에 충분합니다.
scriptum

27

훌륭한 글에도 불구하고, 나는 당신이 당신의 용어를 약간 혼란스럽게 생각합니다. 예를 들어, 호출이 마지막으로 함수를 실행해야 할 때 테일 호출이 발생하는 것이 맞지만, 계속 호출과 관련하여 테일 호출은 함수가 호출 된 연속을 수정하지 않고 해당 호출 만 계속한다는 것을 의미합니다. 연속에 전달 된 값을 업데이트합니다 (원하는 경우). 이것이 꼬리 재귀 함수를 CPS로 변환하는 것이 매우 쉬운 이유입니다 (연속을 매개 변수로 추가하고 결과에서 연속을 호출하기 만하면됩니다).

또한 연속 콜백의 특수한 경우를 호출하는 것이 약간 이상합니다. 어떻게 쉽게 그룹화되는지 알 수 있지만 콜백과 구별해야 할 필요성이 계속되지는 않았습니다. 연속은 실제로 계산을 완료하기 위해 남아 있는 명령 또는 시점 부터 계산의 나머지를 나타냅니다 . 연속을 채워야하는 구멍으로 생각할 수 있습니다. 프로그램의 현재 연속을 캡처 할 수 있으면 연속을 캡처했을 때의 프로그램 상태로 정확하게 돌아갈 수 있습니다. 디버거를보다 쉽게 ​​작성할 수 있습니다.

이와 관련하여 귀하의 질문에 대한 답변은 콜백 이 [콜백의] 발신자가 제공 한 일부 계약에 의해 특정 시점에 호출되는 일반적인 것입니다. 콜백은 원하는만큼 많은 인수를 가질 수 있으며 원하는 방식으로 구성 할 수 있습니다. 따라서 연속 은 반드시 전달 된 값을 해결하는 하나의 인수 프로 시저입니다. 연속은 단일 값에 적용되어야하고 애플리케이션은 마지막에 발생해야합니다. 연속 실행이 완료되면 표현의 실행이 완료되고 언어의 의미에 따라 부작용이 발생하거나 발생하지 않았을 수 있습니다.


3
설명해 주셔서 감사합니다. 당신은 맞습니다. 연속은 실제로 프로그램의 제어 상태를 개선하는 것입니다. 특정 시점의 프로그램 상태에 대한 스냅 샷입니다. 일반 함수처럼 호출 할 수 있다는 사실은 관련이 없습니다. 연속은 실제로 기능하지 않습니다. 반면에 콜백은 실제로 기능입니다. 이것이 연속과 콜백의 실제 차이점입니다. 그럼에도 불구하고 JS는 일류 연속을 지원하지 않습니다. 일류 함수 만. 따라서 JS에서 CPS로 작성된 연속은 단순히 기능입니다. 입력 해 주셔서 감사합니다. =)
Aadit M Shah

4
@AaditMShah 그렇습니다. 연속은 함수 (또는 내가 호출 한 절차) 일 필요는 없습니다. 정의상 그것은 아직 오지 않은 것들의 추상적 인 표현 일뿐입니다. 그러나 Scheme에서도 연속은 프로 시저처럼 호출되어 하나로 전달됩니다. 흠 .. 이것은 연속이 어떻게 보이는지에 대한 똑같이 흥미로운 질문을 제기한다. 이것은 함수 / 프로 시저가 아니다.
dcow

@AaditMShah 내가 여기서 토론을 계속할 정도로 흥미 롭다 : programmers.stackexchange.com/questions/212057/…
dcow

14

짧은 대답은 연속과 콜백의 차이점은 콜백이 호출 된 후 호출이 완료된 시점에서 실행이 재개되고 연속을 호출하면 연속이 생성 된 시점에서 실행이 재개된다는 것입니다. 즉 , 연속은 결코 반환하지 않습니다 .

기능을 고려하십시오.

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

(나는 자바 스크립트가 실제로 일류 연속을 지원하지 않더라도 Javascript 구문을 사용합니다. 이것은 귀하가 귀하의 예를 제시 한 것이기 때문에 Lisp 구문에 익숙하지 않은 사람들에게 더 이해하기 쉽습니다.)

이제 콜백을 전달하면 :

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

그러면 "전", "5"및 "후"라는 세 가지 경고가 표시됩니다.

반면에 콜백과 동일한 작업을 계속 진행하려면 다음과 같이하십시오.

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

"전"과 "5"의 두 가지 경고 만 볼 수 있습니다. 호출 c()내측 add()단부의 실행 add()및 원인을 callcc()리턴하는 단계; 에 의해 반환 된 값 callcc()은 인수로 전달 된 값 c(즉, 합계)입니다.

이런 의미에서 연속 호출은 함수 호출처럼 보이지만 어떤 식 으로든 리턴 문과 유사하거나 예외를 던집니다.

실제로 call / cc를 사용하여 지원하지 않는 언어에 return 문을 추가 할 수 있습니다. 예를 들어, JavaScript에 리턴 문이없는 경우 (대신 Lips 언어와 같이 함수 본문에서 마지막 표현식의 값만 리턴 함) 호출 / cc가있는 경우 다음과 같이 return을 구현할 수 있습니다.

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

호출 return(i)하면 익명 함수의 실행이 종료되고 에서 찾은 callcc()색인이 반환 되는 연속이 호출 됩니다 .itargetmyArray

(NB : "반환"비유가 약간 간단한 방법이 있습니다. 예를 들어, 연속이 함수에서 이스케이프 된 경우 글로벌 어딘가에 저장함으로써 생성 된 함수는 다음과 같습니다. 한 번만 호출하더라도 연속을 만든 여러 번 반환 할 수 있습니다 .)

Call / cc는 예외 처리 (throw and try / catch), 루프 및 기타 여러 제어 구조를 구현하는 데 유사하게 사용할 수 있습니다.

몇 가지 오해를 해결하려면 :

  • 일류 연속을 지원하기 위해 테일 콜 최적화가 반드시 필요한 것은 아닙니다. C 언어조차도 (제한된) 형태의 연속 형식 setjmp()을 사용하여 연속을 생성하고 longjmp()를 호출하는 것을 고려하십시오!

    • 반면에, 테일 콜 최적화없이 연속적인 전달 스타일로 프로그램을 순진하게 작성하려고하면 결국 스택 오버플로가 발생할 수 있습니다.
  • 연속이 하나의 주장만을 취하는 특별한 이유는 없다. 연속에 대한 인수가 call / cc의 반환 값이되고 call / cc는 일반적으로 단일 반환 값을 갖는 것으로 정의되므로 당연히 연속은 정확히 하나를 가져와야합니다. Common Lisp, Go 또는 실제로 Scheme과 같은 여러 반환 값을 지원하는 언어에서는 여러 값을 허용하는 연속을 사용할 수 있습니다.


2
JavaScript 예제에서 오류가 발생한 경우 사과드립니다. 이 답변을 작성하면 필자가 작성한 총 JavaScript 양이 두 배가되었습니다.
cpcallen

이 답변에서 무제한 연속에 대해 이야기하고 있고, 허용 된 답변이 구분 연속에 대해 이야기하고 있음을 올바르게 이해하고 있습니까?
Jozef Mikušinec

1
"연속을 호출하면 연속이 생성 된 시점에서 실행이 재개됩니다."- 현재 연속 을 캡처 하는 것과 연속 을 "만들기" 와 혼동하고 있다고 생각합니다 .
Alexey

@Alexey : 이것은 내가 승인 한 일종의 페도 트리입니다. 그러나 대부분의 언어는 현재 연속을 캡처하는 것 외에 (통합) 연속을 만드는 방법을 제공하지 않습니다.
cpcallen

1
@ jozef : 나는 확실히 무제한 연속에 대해 이야기하고 있습니다. 비록 dcow가 인정한 답변이 연속을 (밀접하게 관련된) 꼬리 호출과 구별하지 못하지만 Aadit의 의도라고 생각합니다. 구분 된 연속은 어쨌든 fuction / procedure와 동일합니다 : community.schemewiki.org/ ? composable-continuations-tutorial
cpcallen
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.