왜 트램펄린이 작동합니까?


104

기능적인 JavaScript를 수행하고 있습니다. Tail-Call Optimization 이 구현 되었다고 생각 했지만 그것이 잘못되었습니다. 따라서 나는 나 자신에게 Trampolining 을 가르쳐야했다 . 여기와 다른 곳에서 약간의 독서를 한 후, 기본 사항을 알아 내고 첫 번째 트램폴린을 만들 수있었습니다.

/*not the fanciest, it's just meant to
reenforce that I know what I'm doing.*/

function loopy(x){
    if (x<10000000){ 
        return function(){
            return loopy(x+1)
        }
    }else{
        return x;
    }
};

function trampoline(foo){
    while(foo && typeof foo === 'function'){
        foo = foo();
    }
    return foo;
/*I've seen trampolines without this,
mine wouldn't return anything unless
I had it though. Just goes to show I
only half know what I'm doing.*/
};

alert(trampoline(loopy(0)));

내 가장 큰 문제는 이것이 왜 작동하는지 모르겠다는 것입니다. 재귀 루프를 사용하는 대신 while 루프에서 함수를 다시 실행한다는 아이디어를 얻었습니다. 예외적으로 기술적으로 내 기본 기능에는 이미 재귀 루프가 있습니다. 기본 loopy기능을 실행하고 있지 않지만 내부 기능을 실행하고 있습니다. foo = foo()스택 오버플로의 원인은 무엇입니까 ? 그리고 foo = foo()기술적으로 돌연변이 가 아니 거나 뭔가 빠졌습니까? 아마도 그것은 단지 필요한 악일 것입니다. 또는 일부 구문이 누락되었습니다.

그것을 이해하는 방법조차 있습니까? 아니면 어떻게 든 작동하는 해킹입니까? 나는 다른 모든 것을 통해 나아갈 수 있었지만, 이것은 나를 혼란스럽게합니다.


5
네,하지만 여전히 재귀입니다. 자체 호출하지 않기loopy 때문에 오버플로 하지 않습니다 .
tkausl

4
"TCO가 구현되었다고 생각했지만 그것이 잘못되었습니다." 대부분의 시나리오에서 V8 이상이었습니다. 당신은 V8에서 활성화 할 노드를 말함으로써 노드의 최근 버전에서 예를 들어 사용할 수 있습니다 : stackoverflow.com/a/30369729/157247 크롬의 크롬 (51) 이후 (에 "실험"플래그 뒤에) 그것을했다
TJ 크라우

125
트램펄린이 처짐에 따라 사용자의 운동 에너지는 탄성 전위 에너지로 변환 된 다음, 반동 할 때 운동 에너지로 되돌아갑니다.
immibis

66
@immibis, 이것이 어떤 Stack Exchange 사이트인지 확인하지 않고 여기에 온 모든 사람을 대신하여 감사합니다.
user1717828

4
@jpaugh "호핑"을 의미 했습니까? ;-)
Hulk

답변:


89

뇌가 기능에 반항하는 이유 loopy()일관성이없는 유형 이기 때문입니다 .

function loopy(x){
    if (x<10000000){ 
        return function(){ // On this line it returns a function...
            // (This is not part of loopy(), this is the function we are returning.)
            return loopy(x+1)
        }
    }else{
        return x; // ...but on this line it returns an integer!
    }
};

꽤 많은 언어로도 이런 일을 할 수 없거나 적어도 어떤 종류의 말이되는지 이해하기 위해 더 많은 타이핑을 요구합니다. 실제로 그렇지 않기 때문입니다. 함수와 정수는 완전히 다른 종류의 객체입니다.

따라서 while 루프를주의 깊게 살펴 보겠습니다.

while(foo && typeof foo === 'function'){
    foo = foo();
}

처음 foo과 같다 loopy(0). 무엇입니까 loopy(0)? 글쎄, 그것은 10000000보다 작으므로 우리는 얻는다 function(){return loopy(1)}. 그것은 진실한 가치이며, 함수이기 때문에 루프가 계속 진행됩니다.

이제 우리는 왔습니다 foo = foo(). foo()와 동일합니다 loopy(1). 1은 여전히 ​​10000000보다 작으므로를 반환 function(){return loopy(2)}하고에 할당합니다 foo.

foo여전히 함수이므로, foo가 같을 때까지 계속 진행합니다 function(){return loopy(10000000)}. 그것은 함수이기 때문에 우리는 foo = foo()한 번 더하지만, 이번에 loopy(10000000)는 x를 호출 할 때 x는 10000000 이상이므로 x를 다시 얻습니다. 10000000도 함수가 아니기 때문에 while 루프도 종료됩니다.


1
의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
yannis

실제로는 합계 유형입니다. 때로는 변형으로 알려져 있습니다. 동적 언어 는 모든 값에 태그가 지정되어 있기 때문에이를 쉽게 지원하는 반면, 정적으로 형식이 지정된 언어에서는 함수가 변형을 반환하도록 지정해야합니다. 트램폴린은 예를 들어 C ++ 또는 Haskell에서 쉽게 가능합니다.
GManNickG

2
@GManNickG : 예, "더 많은 타이핑"을 의미합니다. C에서는 공용체를 선언하고, 공용체를 태그하는 구조체를 선언하고, 한쪽 끝에서 구조체를 압축하여 압축을 풀고, 한쪽 끝에서 유니온을 압축하여 압축을 풀고, 구조체가 거주하는 메모리를 누가 소유했는지 파악해야합니다. . C ++은 그보다 코드가 적을 가능성이 높지만 개념적으로 C보다 덜 복잡하지는 않지만 OP의 Javascript보다 더 장황합니다.
Kevin

물론, 나는 당신이 그것을 이상하거나 이해하지 못하는 것에 대한 강조가 약간 강하다고 생각합니다. :)
GManNickG

173

Kevin 은이 특정 코드 스 니펫이 어떻게 작동하는지 (간단히 이해할 수없는 이유와 함께) 간결하게 지적하지만 일반 작업 에서 트램폴린 어떻게 작동 하는지에 대한 정보를 추가하고 싶었습니다 .

TCO (꼬리 호출 최적화)가 없으면 모든 함수 호출 은 현재 실행 스택에 스택 프레임 을 추가합니다 . 숫자 카운트 다운을 인쇄하는 기능이 있다고 가정합니다.

function countdown(n) {
  if (n === 0) {
    console.log("Blastoff!");
  } else {
    console.log("Launch in " + n);
    countdown(n - 1);
  }
}

호출하면 countdown(3)TCO없이 호출 스택이 어떻게 보이는지 분석해 봅시다.

> countdown(3);
// stack: countdown(3)
Launch in 3
// stack: countdown(3), countdown(2)
Launch in 2
// stack: countdown(3), countdown(2), countdown(1)
Launch in 1
// stack: countdown(3), countdown(2), countdown(1), countdown(0)
Blastoff!
// returns, stack: countdown(3), countdown(2), countdown(1)
// returns, stack: countdown(3), countdown(2)
// returns, stack: countdown(3)
// returns, stack is empty

TCO를 사용하면 각 재귀 호출 countdown테일 위치 에 있으므로 (호출 결과를 반환하는 것 외에는 아무것도 남지 않습니다) 스택 프레임이 할당되지 않습니다. TCO가 없으면 스택이 약간 커 n집니다.

Trampolining은 countdown함수 주위에 랩퍼를 삽입하여이 제한 사항을 해결합니다 . 그런 다음 countdown재귀 호출을 수행하지 않고 대신 즉시 함수를 호출로 반환합니다. 구현 예는 다음과 같습니다.

function trampoline(firstHop) {
  nextHop = firstHop();
  while (nextHop) {
    nextHop = nextHop()
  }
}

function countdown(n) {
  trampoline(() => countdownHop(n));
}

function countdownHop(n) {
  if (n === 0) {
    console.log("Blastoff!");
  } else {
    console.log("Launch in " + n);
    return () => countdownHop(n-1);
  }
}

이것이 어떻게 작동하는지 더 잘 이해하려면 호출 스택을 살펴 보겠습니다.

> countdown(3);
// stack: countdown(3)
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(3)
Launch in 3
// return next hop from countdownHop(3)
// stack: countdown(3), trampoline
// trampoline sees hop returned another hop function, calls it
// stack: countdown(3), trampoline, countdownHop(2)
Launch in 2
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(1)
Launch in 1
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(0)
Blastoff!
// stack: countdown(3), trampoline
// stack: countdown(3)
// stack is empty

각각의 단계 countdownHop기능을 포기 하는 대신 그게 것에 대해 설명 호출하는 함수를 반환, 다음에 어떤 일이 일어나는지를 직접 제어 같은 다음 일이 있습니다. 그 다음에 트램펄린 함수는 이것을 가져 와서 호출 한 다음 "다음 단계"가 없을 때까지 리턴 하는 모든 함수를 호출 합니다. 제어 흐름이 함수가 직접 되풀이되는 대신 각 재귀 호출과 트램펄린 구현 사이에서 "튀어 오므로"이를 트램폴린이라고합니다. 재귀 호출 을 하는 사람에 대한 제어를 포기함으로써 트램펄린 기능은 스택이 너무 커지지 않도록 할 수 있습니다. 참고 사항 :이 구현은 trampoline단순성을 위해 값을 반환하지 않습니다.

이것이 좋은 아이디어인지 아는 것은 까다로울 수 있습니다. 각 단계에서 새 클로저를 할당하면 성능이 저하 될 수 있습니다. 영리한 최적화를 통해이 기능을 실현할 수는 있지만 알 수는 없습니다. Trampolining은 언어 구현이 최대 호출 스택 크기를 설정하는 경우와 같이 어려운 재귀 한계를 극복하는 데 주로 유용합니다.


18

트램펄린이 함수를 남용하는 대신 전용 리턴 유형으로 구현했는지 이해하기가 더 쉬울 수 있습니다.

class Result {}
// poor man's case classes
class Recurse extends Result {
    constructor(a) { this.arg = a; }
}
class Return extends Result {
    constructor(v) { this.value = v; }
}

function loopy(x) {
    if (x<10000000)
        return new Recurse(x+1);
    else
        return new Return(x);
}

function trampoline(fn, x) {
    while (true) {
        const res = fn(x);
        if (res instanceof Recurse)
            x = res.arg;
        else if (res instanceof Return)
            return res.value;
    }
}

alert(trampoline(loopy, 0));

이것을 trampoline재귀 사례가 함수가 다른 함수를 반환 할 때이고 기본 사례가 다른 것을 반환 할 때 의 버전과 대조하십시오 .

foo = foo()스택 오버플로의 원인은 무엇입니까 ?

더 이상 자신을 부르지 않습니다. 대신, Result재귀를 계속할지 또는 나갈지를 전달 하는 결과 (내 구현에서는 말 그대로 )를 반환합니다 .

그리고 foo = foo()기술적으로 돌연변이 가 아니 거나 뭔가 빠졌습니까? 아마도 그것은 단지 필요한 악일 것입니다.

예, 이것은 루프의 필수 악입니다. 하나는 trampoline돌연변이없이 쓸 수 있지만 재귀가 다시 필요합니다.

function trampoline(fn, x) {
    const res = fn(x);
    if (res instanceof Recurse)
        return trampoline(fn, res.arg);
    else if (res instanceof Return)
        return res.value;
}

여전히 트램폴린 기능이 더 잘 작동한다는 아이디어를 보여줍니다.

트램 폴링의 요점은 재귀를 반환 값으로 사용하려는 함수에서 꼬리 재귀 호출을 추상화하고 한 곳에서만 실제 재귀를 수행하는 것입니다.이 trampoline기능은 단일 위치에서 최적화하여 고리.


foo = foo()로컬 상태를 수정한다는 의미에서 돌연변이이지만, 일반적으로 기본 함수 객체를 실제로 수정하지 않을 때 재 할당을 고려하여 반환하는 함수 (또는 값)로 대체한다고 생각합니다.
JAB

@JAB 예, foo포함 된 값을 변경하는 것을 의미하지는 않았으며 변수 만 수정되었습니다. while당신이 종료 할 경우 루프는이 경우 변수에 일부 변경 가능한 상태를 필요로 foo하거나 x.
Bergi

나는 꼬리 호출 최적화, 트램폴린 등에 관한 스택 오버플로 질문에 대한 답변 에서 잠시 동안 이와 같은 것을했습니다.
Joshua Taylor

2
돌연변이가없는 버전이의 재귀 호출을 변환 한 fn에 재귀 호출에 trampoline- 나는 그 개선의 확실하지 않다.
Michael Anderson

1
@MichaelAnderson 그것은 단지 추상화를 보여주기위한 것입니다. 물론 재귀 트램폴린은 유용하지 않습니다.
Bergi
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.