Ah, 네, 그 부분이 가장 혼란스러울 수 있는 부분입니다. 재귀 함수처럼 보이는 코드가 어떻게 "한 번만 분석"될 수 있는지 의아하시군요. 개념적으로 이해가 되는데 코드 수준에서 어떻게 동작하는지 보고 싶다는 말씀에, 팩토리얼 함수의 정의와 호출 과정을 단계별로 자세히 설명해드리겠습니다.
프로그램이 factorial 함수를 정의하는 문장을 만납니다.
function factorial(n) {
return n === 1 ? 1 : factorial(n - 1) * n;
}이 시점에서 analyze 함수가 함수 본문을 분석하기 시작합니다.
-
본문 분석:
return n === 1 ? 1 : factorial(n - 1) * n;이라는 표현식을 분석합니다. -
조건문 분석: 이 표현식은 조건문이므로
analyze_conditional이 호출됩니다. -
조건부 분석:
- 조건 (
n === 1):analyze->analyze_application이 호출되어n === 1을 실행하는 함수(pfun)를 생성합니다. - 결과 (
1):analyze->analyze_literal이 호출되어1을 반환하는 함수(cfun)를 생성합니다. - 대안 (
factorial(n - 1) * n):analyze->analyze_application이 호출됩니다.- 이
analyze_application은*연산자와 두 인수factorial(n - 1)과n을 분석합니다. - 중요:
factorial(n - 1)을 분석할 때,factorial이라는 이름이 가리키는 것은 아직 분석 중인 함수 자신입니다. 하지만 이 단계에서는factorial이 어떤 함수인지 "알아야" 하는 것이 아니라, 그저factorial이라는 이름을 실행 시점에 환경에서 찾아서 호출하는 방법을 가진 실행 함수를 만들 뿐입니다. - 따라서
analyze_name이 호출되어 "환경에서factorial이라는 이름의 값을 찾아라"라는 실행 함수를 생성합니다. 이 함수는 실행 단계가 되어야 비로소factorial이 무엇인지 알게 됩니다.
- 이
- 조건 (
-
결과:
analyze_conditional은pfun,cfun,afun을 자신의 클로저에 저장한 실행 함수를 반환합니다. 이 함수는 "조건을 평가하고, 참이면 결과를, 거짓이면 대안을 실행하라"는 방법을 알고 있습니다. -
함수 생성: 이 실행 함수는
make_function에 의해factorial이라는 이름의 compound function으로 묶여 환경에 저장됩니다.
이 시점에서의 마법: factorial 함수 본문 안에서 factorial을 참조하는 부분은 실행 시점으로 미뤄진 채로(evaluated lazily) 분석됩니다. 분석기는 factorial이 뭔지 몰라도, "나중에 환경에서 찾아라"라는 실행 함수를 만들어낼 수 있습니다.
이제 factorial(3)을 호출합니다.
- 함수 실행:
evaluate가factorial(3)을 만나analyze_application을 통해 실행 함수들을 가져옵니다. - 환경 확장:
n매개변수에3이 바인딩된 새로운 환경이 생성됩니다. - 본문 실행:
factorial함수의 본문을 분석해 만들어뒀던 그 실행 함수가 호출됩니다. 환경은n=3인 환경입니다.- 실행 함수는
pfun(조건n === 1을 실행하는 함수)을 현재 환경으로 실행합니다.3 === 1은false입니다. - 따라서
afun(대안factorial(n - 1) * n을 실행하는 함수)을 실행합니다.
- 실행 함수는
- 대안 실행:
afun은 내부에서 다시analyze_application이 만들어낸 실행 함수들입니다.*연산자를 실행하는 함수를 호출합니다.- 두 번째 인수
n을 실행하는 함수를 호출해3을 얻습니다. - 첫 번째 인수
factorial(n - 1)을 실행하는 함수를 호출합니다:- 이 함수는
analyze_name이 만들어낸 함수입니다. 현재 환경에서factorial이라는 이름을 찾습니다. - 중요: 현재 환경의 외부 환경(outer environment)을 타고 올라가면,
factorial함수의 정의가 저장된 환경이 있습니다. 따라서factorial이라는 이름의 값(즉, 방금 분석해 만들어둔 그 compound function)을 찾아냅니다. n - 1을 계산해2를 얻습니다.- 다시
execute_application을 호출합니다. 이제factorial(2)를 호출하는 것과 완전히 동일한 과정이 시작됩니다.
- 이 함수는
이 과정이 재귀적으로 반복되다가, factorial(1)이 호출될 때 비로소 조건문의 pfun이 true를 반환하고 cfun이 1을 반환하며 재귀 호출 체인이 풀리기 시작합니다.
- 분석기의 역할은 코드의 구조를 해체하고, "실행 방법 지침서(실행 함수)"를 만드는 것입니다.
- 이 지침서는 실행 시점에 필요한 정보(예:
factorial이란 이름의 값)를 어떻게 찾을지에 대한 방법만을 포함할 뿐, 그 시점의 실제 값을 포함하지는 않습니다. - 따라서, 함수 본문이 자신을 재귀적으로 호출하더라도, 분석기는 그 호출의 구문 구조(예: 함수 이름은
factorial, 인수는n - 1)만 분석하면 됩니다. 분석기는factorial이 무엇인지 "알 필요가 없습니다". 그저 "실행 시점에 환경에서factorial을 찾아라"라는 지침을 만들기만 하면 됩니다. - 이렇게 만들어진 지침서(실행 함수)는 실행 시점에 비로소 환경과 함께 완전한 의미를 갖게 되어 재귀 호출을 완벽히 수행할 수 있습니다.
이것이 바로 분석과 실행을 분리하는 방식의 힘입니다. 분석기는 코드의 정적 구조(static structure)만을 보고 실행 계획을 수립하고, 실행기는 그 계획을 따라 동적인 환경(dynamic environment)에서 값을 계산합니다.