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


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.