이것이 재귀 프로 시저를 꼬리 재귀로 변환하는 일반적인 방법입니까?


13

내가 변환하는 일반적인 방법을 발견 한 것 같습니다 어떤 꼬리 재귀에 재귀 절차를 :

  1. 추가 "result"매개 변수를 사용하여 헬퍼 서브 프로 시저를 정의하십시오.
  2. 프로 시저의 리턴 값에 적용될 항목을 해당 매개 변수에 적용하십시오.
  3. 시작하려면이 도우미 절차를 호출하십시오. "result"매개 변수의 초기 값은 재귀 프로세스의 종료점에 대한 값이므로 재귀 프로세스가 축소되기 시작하는 위치에서 결과 반복 프로세스가 시작됩니다.

예를 들어, 변환 할 원래 재귀 프로시 저는 다음과 같습니다 ( SICP 연습 1.17 ).

(define (fast-multiply a b)
  (define (double num)
    (* num 2))
  (define (half num)
    (/ num 2))
  (cond ((= b 0) 0)
        ((even? b) (double (fast-multiply a (half b))))
        (else (+ (fast-multiply a (- b 1)) a))))

다음은 변환 된 꼬리 재귀 프로 시저입니다 ( SICP 연습 1.18 ).

(define (fast-multiply a b)
  (define (double n)
    (* n 2))
  (define (half n)
    (/ n 2))
  (define (multi-iter a b product)
    (cond ((= b 0) product)
          ((even? b) (multi-iter a (half b) (double product)))
          (else (multi-iter a (- b 1) (+ product a)))))
  (multi-iter a b 0))

누군가 이것을 증명하거나 반증 할 수 있습니까?


1
영형(로그)

두 번째 생각 : b2의 거듭 제곱을 선택하면 처음 product에 0으로 설정 하는 것이 옳지 않다는 것을 알 수 있습니다. 그러나 b홀수 일 때는 1로 변경해도 작동하지 않습니다 . 어쩌면 두 개의 다른 누산기 매개 변수가 필요할까요?
j_random_hacker

3
비 꼬리 재귀 정의의 변환을 실제로 정의하지 않았으므로 일부 결과 매개 변수를 추가하고 누적에 사용하는 것은 매우 모호하며 두 번의 재귀 호출이있는 더 복잡한 경우 (예 : 나무 순회)로 거의 일반화되지 않습니다. "연속"에 대한보다 정확한 아이디어가 존재하는데, 여기서 작업의 일부를 수행 한 다음 "연속"기능을 인수하여 지금까지 수행 한 작업을 매개 변수로받습니다. 이를 연속 전달 스타일 (cps)이라고합니다 ( en.wikipedia.org/wiki/Continuation-passing_style 참조) .
Ariel

4
이 슬라이드 fsl.cs.illinois.edu/images/d/d5/CS422-Fall-2006-13.pdf 에는 cps 변환에 대한 설명이 포함되어 있으며, 임의 변환을 수행 할 수 있습니다 (테일 호출이 아닌 함수 정의 일 수 있음). 꼬리 호출만으로 동등한 표현식으로 변환하십시오.
Ariel

@j_random_hacker 예, "변환 된"절차가 실제로 잘못되었음을 알 수 있습니다.
nalzok

답변:


12

이 시점에서 알고리즘을 설명하기에 너무 모호합니다. 그러나 고려해야 할 사항이 있습니다.

CPS

실제로 모든 코드를 테일 콜만 사용하는 형식으로 변환하는 방법이 있습니다. 이것이 CPS 변환입니다. CPS ( Continuation-Passing Style )는 각 함수에 연속을 전달하여 코드를 표현하는 형식입니다. 연속은 "나머지 계산"을 나타내는 추상적 인 개념입니다. CPS 형식으로 표현 된 코드에서 연속 을 구체화 하는 자연스러운 방법 은 값을 받아들이는 함수입니다. CPS에서는 값을 반환하는 함수 대신 현재 연속을 나타내는 함수를 함수가 "반환"하는 것에 적용합니다.

예를 들어 다음 기능을 고려하십시오.

(lambda (a b c d)
  (+ (- a b) (* c d)))

이것은 CPS에서 다음과 같이 표현 될 수 있습니다.

(lambda (k a b c d)
  (- (lambda (v1)
       (* (lambda (v2)
            (+ k v1 v2))
          a b))
     c d))

추악하고 종종 느리지 만 특정 장점이 있습니다.

  • 변환은 완전히 자동화 될 수 있습니다. 따라서 CPS 형식으로 코드를 작성하거나 볼 필요가 없습니다.
  • 썽킹 및 trampolining 과 함께 사용하여 테일 콜 최적화를 제공하지 않는 언어로 테일 콜 최적화를 제공 할 수 있습니다. 직접 테일 재귀 함수의 테일 콜 최적화는 재귀 호출을 루프로 변환하는 등의 다른 방법을 통해 수행 할 수 있습니다. 그러나 간접 재귀는 이러한 방식으로 변환하는 것만 큼 간단하지 않습니다.
  • CPS를 통해 연속은 일류 개체가됩니다. 연속성이 제어의 본질이기 때문에 언어의 특별한 지원 없이도 거의 모든 제어 연산자를 라이브러리로 구현할 수 있습니다. 예를 들어 goto, exceptions 및 cooperative threading은 모두 연속을 사용하여 모델링 할 수 있습니다.

TCO

테일 재귀 (또는 일반적으로 테일 콜)에 관심을 가지는 유일한 이유는 테일 콜 최적화 (TCO)를위한 것입니다. 그래서 더 좋은 질문은 "꼬리 호출 최적화 가능한 변환 수율 코드입니까?"입니다.

CPS를 다시 한 번 고려하면 그 특성 중 하나는 CPS로 표현 된 코드가 꼬리 호출만으로 구성된다는 것입니다. 모든 것이 테일 콜이므로 리턴 포인트를 스택에 저장할 필요가 없습니다. CPS 형식의 모든 코드는 테일 콜에 최적화 되어야합니다 .

글쎄요 우리는 스택을 제거한 것처럼 보일 수 있지만, 우리가 한 것은 단지 우리가 나타내는 방식을 바꾸는 것입니다. 스택은 이제 연속을 나타내는 클로저의 일부입니다. 따라서 CPS는 모든 코드 테일 콜을 마술처럼 최적화하지는 않습니다.

CPS가 모든 TCO를 만들 수 없다면 직접 재귀를위한 변형이 있습니까? 아니요, 일반적으로 아닙니다. 일부 재귀는 선형이지만 일부는 그렇지 않습니다. 비선형 (예 : 트리) 재귀는 단순히 어딘가에 가변적 인 양의 상태를 유지 해야합니다 .


" TCO "하위 섹션에있을 때는 약간 혼동됩니다. "꼬리 호출 최적화"라고 말하면 실제로 "일정한 메모리 사용"을 의미합니다. 동적 메모리 사용량이 일정하지 않다고해서 호출이 실제로 미미 하고 스택 사용량이 무한히 증가 하지 않는다는 사실을 부정하지는 않습니다 . SICP는 이러한 계산을 "반복적"이라고 부릅니다. "TCO이지만 여전히 반복적이지는 않습니다"라고 말하는 것이 더 나을 수 있습니다.
Will Ness

@WillNess 우리는 여전히 콜 스택을 가지고 있습니다. 하드웨어 스택이 아닌 힙을 사용하기 때문에 구조가 변경되지 않습니다 . 결국, 이름에 "스택"이있는 동적 힙 메모리를 기반으로하는 많은 데이터 구조가 있습니다.
Nathan Davis

여기서 유일한 요점은 일부 언어에는 콜 스택 사용에 대한 제한이 있다는 것입니다.
Will Ness
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.