"재귀는 두 개의 약속이라는 걸 알아채기 전까지는 마법이나 광기처럼 느껴져. '이건 바로 답할 수 있는 경우야', 그리고 '나머지 모든 경우는 그쪽으로 줄여 갈게.' 둘 다 쥐고 있으면 재귀는 안전하고, 하나라도 놓치면 나선으로 떨어져."
자기 자신을 부르는 함수
재귀는 같은 문제의 더 작은 버전에 자기 자신을 불러서 문제를 푸는 함수야. 순환처럼 들리지? 실제로 순환이 맞아. 그 순환을 끊는 조각 하나만 빼면. 제대로 된 재귀는 정확히 두 부분을 가져.
- base case. 더 재귀하지 않고 즉시 답할 수 있을 만큼 작은 문제야. 내려가기를 멈추는 바닥이지.
- 재귀 케이스. 더 작은 버전을 자기 자신을 불러 풀고, 그걸로 지금의 답을 지어. 언제나 base case 쪽으로 움직이면서.
팩토리얼은 음이 아닌 정수 n에 대해 factorial(0) = 1, factorial(n) = n × factorial(n−1)로 정의할 수 있어. 함수는 n < 0을 거부해야 하고, 호출할 때마다 n이 0 쪽으로 줄어야 하고. 입력 도메인과 줄어드는 척도를 함께 적어 두는 게 재귀의 계약이야.
깨지는 두 가지 방법
재귀는 정확히 두 가지 방식으로 실패하는데 둘 다 계약 위반이야. 하나는 base case가 없는 경우야. 함수가 자기를 부르는 걸 멈추지 않아서 콜 스택이 가득 차고 Python이 RecursionError를 던져. 다른 하나는 base case로 다가가지 않는 경우고. base case가 있어도 재귀 호출이 실제로 더 작지 않으면, 그러니까 f(n−1) 대신 f(n)을 부르면 바닥에 영영 못 닿아서 똑같이 죽어. 그래서 계약이 양면인 거야. base case가 있어야 하고, 동시에 모든 재귀 호출이 거기로 눈에 보이게 가까워져야 해.
재귀는 돌아가는 귀납법이야
수학적 귀납법을 본 적이 있으면 재귀가 익숙할 거야. 같은 아이디어거든. 귀납법은 base case(n=0에서 성립)와 귀납 단계(n−1에서 성립하면 n에서도 성립)를 세워서 명제를 증명하지. 재귀는 같은 방식으로 계산해. base case를 두고, '더 작은 호출이 맞다고 가정하고 그 위에 짓는' 거야. 그 가정이 트리 트랙에서 본 믿음의 도약이고. 재귀 케이스를 쓸 때는 factorial(n−1)이 이미 맞는 답을 준다고 믿고 결합만 신경 쓰면 돼. 스택 전체를 머릿속으로 따라가지 마. base case와 귀납이 정확성을 보장해 주니까.
피파의 고백
n−1이 아니라 n에 재귀해서 진전이 없었지). 아빠가 지금도 돌려 보는 체크리스트를 줬어. "네 base case 를 가리켜. 이제 입력을 더 작게 만드는 줄을 가리켜." 둘 다 못 가리키면 망가진 거야. 이 두 질문이 재귀를 알 수 없는 크래시의 원천에서 5초면 검증되는 것으로 바꿔 놨어.