재귀 이해하기

토파즈에서 재귀의 아름다운 세계를 탐험하세요. 기본 재귀부터 고급 테크닉, 최적화, 실용적인 응용까지 완벽 가이드입니다.

재귀는 프로그래밍의 가장 아름답고 강력한 개념 중 하나입니다. 토파즈에서 재귀의 우아함을 발견해보세요! 🌀

🌱 재귀의 기본 개념

재귀란 무엇인가?

재귀는 함수가 자기 자신을 호출하는 프로그래밍 기법입니다. 복잡한 문제를 더 작은 동일한 문제로 분해하여 해결합니다.

// 가장 간단한 재귀 - 팩토리얼
function 팩토리얼(n: int) -> int {
    match n {
        case 0 | 1 => 1                    // 기저 조건 (Base Case)
        case _ => n * 팩토리얼(n - 1)      // 재귀 호출
    }
} test {
    assert 팩토리얼(0) == 1
    assert 팩토리얼(1) == 1
    assert 팩토리얼(5) == 120
    assert 팩토리얼(10) == 3628800
}

// 피보나치 수열
function 피보나치(n: int) -> int {
    match n {
        case 0 => 0
        case 1 => 1
        case _ => 피보나치(n - 1) + 피보나치(n - 2)
    }
} test {
    assert 피보나치(0) == 0
    assert 피보나치(1) == 1
    assert 피보나치(8) == 21
    assert 피보나치(12) == 144
}

print("팩토리얼(5) = {팩토리얼(5)}")
print("피보나치(8) = {피보나치(8)}")

재귀의 핵심 요소

  1. 기저 조건 (Base Case): 재귀를 멈추는 조건
  2. 재귀 관계 (Recursive Relation): 자기 자신을 호출하는 부분
  3. 수렴성 (Convergence): 반드시 기저 조건에 도달해야 함

🎨 재귀의 다양한 패턴

선형 재귀 (Linear Recursion)

// 배열의 합 구하기
function 배열합(배열: Array<int>, 인덱스: int = 0) -> int {
    if 인덱스 >= 배열.length {
        return 0
    }
    return 배열[인덱스] + 배열합(배열, 인덱스 + 1)
}

// 문자열 뒤집기
function 문자열뒤집기(문자열: string) -> string {
    if 문자열.length <= 1 {
        return 문자열
    }
    
    let 첫글자 = 문자열.charAt(0)
    let 나머지 = 문자열.substring(1)
    
    return 문자열뒤집기(나머지) + 첫글자
}

// 최대공약수 (유클리드 호제법)
function 최대공약수(a: int, b: int) -> int {
    match b {
        case 0 => a
        case _ => 최대공약수(b, a % b)
    }
} test {
    let 숫자들 = [1, 2, 3, 4, 5]
    assert 배열합(숫자들) == 15
    
    assert 문자열뒤집기("hello") == "olleh"
    assert 문자열뒤집기("Topaz") == "zapoT"
    
    assert 최대공약수(48, 18) == 6
    assert 최대공약수(100, 25) == 25
}

print("배열 합: {배열합([1, 2, 3, 4, 5])}")
print("문자열 뒤집기: {문자열뒤집기("안녕하세요")}")
print("최대공약수(48, 18): {최대공약수(48, 18)}")

분할 정복 (Divide and Conquer)

// 이진 탐색
function 이진탐색<T>(
    배열: Array<T>,
    대상: T,
    시작: int = 0,
: int = -1
) -> Option<int>
where
    T: PartialOrd + PartialEq
{
    let 실제끝 = if== -1 { 배열.length - 1 } else { 끝 }
    
    if 시작 > 실제끝 {
        return None
    }
    
    let 중간 = (시작 + 실제끝) / 2
    
    match 배열[중간] {
        case x if x == 대상 => Some(중간)
        case x if x > 대상 => 이진탐색(배열, 대상, 시작, 중간 - 1)
        case _ => 이진탐색(배열, 대상, 중간 + 1, 실제끝)
    }
}

// 병합 정렬
function 병합정렬(배열: Array<int>) -> Array<int> {
    if 배열.length <= 1 {
        return 배열
    }
    
    let 중간 = 배열.length / 2
    let 왼쪽 = 배열[0..중간].to_vec()
    let 오른쪽 = 배열[중간..].to_vec()
    
    let 정렬된왼쪽 = 병합정렬(왼쪽)
    let 정렬된오른쪽 = 병합정렬(오른쪽)
    
    return 병합(정렬된왼쪽, 정렬된오른쪽)
}

function 병합(왼쪽: Array<int>, 오른쪽: Array<int>) -> Array<int> {
    let mut 결과: Array<int> = []
    let mut i = 0
    let mut j = 0
    
    while i < 왼쪽.length && j < 오른쪽.length {
        if 왼쪽[i] <= 오른쪽[j] {
            결과.push(왼쪽[i])
            i += 1
        } else {
            결과.push(오른쪽[j])
            j += 1
        }
    }
    
    // 남은 요소들 추가
    결과.extend(&왼쪽[i..])
    결과.extend(&오른쪽[j..])
    
    return 결과
}

// 퀵 정렬
function 퀵정렬(배열: Array<int>) -> Array<int> {
    if 배열.length <= 1 {
        return 배열
    }
    
    let 피벗 = 배열[배열.length / 2]
    let 작은것들 = 배열.filter(|&x| x < 피벗)
    let 같은것들 = 배열.filter(|&x| x == 피벗)
    let 큰것들 = 배열.filter(|&x| x > 피벗)
    
    return [
        ...퀵정렬(작은것들),
        ...같은것들,
        ...퀵정렬(큰것들)
    ]
} test {
    let 정렬된배열 = [1, 3, 5, 7, 9, 11, 13]
    assert 이진탐색(정렬된배열, 7) == Some(3)
    assert 이진탐색(정렬된배열, 14) == None
    
    let 무작위배열 = [64, 34, 25, 12, 22, 11, 90]
    let 정렬결과1 = 병합정렬(무작위배열.clone())
    let 정렬결과2 = 퀵정렬(무작위배열.clone())
    
    assert 정렬결과1 == [11, 12, 22, 25, 34, 64, 90]
    assert 정렬결과2 == [11, 12, 22, 25, 34, 64, 90]
}

print("이진 탐색 결과: {이진탐색([1, 3, 5, 7, 9], 5)}")
print("병합 정렬: {병합정렬([3, 1, 4, 1, 5, 9])}")
print("퀵 정렬: {퀵정렬([3, 1, 4, 1, 5, 9])}")

트리 재귀 (Tree Recursion)

// 이진 트리 구조
enum 이진트리<T> {
    Empty,
    Node {
: T,
        왼쪽: Box<이진트리<T>>,
        오른쪽: Box<이진트리<T>>
    }
}

impl<T> 이진트리<T>
where
    T: Clone + std::fmt::Display + PartialOrd
{
    function 새로운노드(값: T) -> 이진트리<T> {
        return 이진트리::Node {
            값,
            왼쪽: Box::new(이진트리::Empty),
            오른쪽: Box::new(이진트리::Empty)
        }
    }
    
    // 트리에 값 삽입
    function 삽입(&mut self, 새값: T) {
        match self {
            case 이진트리::Empty => {
                *self = 이진트리::새로운노드(새값)
            }
            case 이진트리::Node { 값, 왼쪽, 오른쪽 } => {
                if 새값 <= *값 {
                    왼쪽.삽입(새값)
                } else {
                    오른쪽.삽입(새값)
                }
            }
        }
    }
    
    // 트리 검색
    function 검색(&self, 대상: &T) -> bool {
        match self {
            case 이진트리::Empty => false
            case 이진트리::Node { 값, 왼쪽, 오른쪽 } => {
                if 대상 == 값 {
                    return true
                } else if 대상 < 값 {
                    return 왼쪽.검색(대상)
                } else {
                    return 오른쪽.검색(대상)
                }
            }
        }
    }
    
    // 중위 순회 (In-order Traversal)
    function 중위순회(&self) -> Array<T> {
        match self {
            case 이진트리::Empty => []
            case 이진트리::Node { 값, 왼쪽, 오른쪽 } => {
                let mut 결과 = 왼쪽.중위순회()
                결과.push(값.clone())
                결과.extend(오른쪽.중위순회())
                return 결과
            }
        }
    }
    
    // 트리 높이 계산
    function 높이(&self) -> int {
        match self {
            case 이진트리::Empty => 0
            case 이진트리::Node { 왼쪽, 오른쪽, .. } => {
                let 왼쪽높이 = 왼쪽.높이()
                let 오른쪽높이 = 오른쪽.높이()
                return 1 + std::cmp::max(왼쪽높이, 오른쪽높이)
            }
        }
    }
    
    // 노드 수 계산
    function 노드수(&self) -> int {
        match self {
            case 이진트리::Empty => 0
            case 이진트리::Node { 왼쪽, 오른쪽, .. } => {
                return 1 + 왼쪽.노드수() + 오른쪽.노드수()
            }
        }
    }
} test {
    let mut 트리 = 이진트리::Empty
    
    // 값들 삽입
    let 값들 = [5, 3, 7, 1, 9, 4, 6]
    forin 값들 {
        트리.삽입(값)
    }
    
    // 검색 테스트
    assert 트리.검색(&5) == true
    assert 트리.검색(&10) == false
    
    // 중위 순회 (정렬된 순서)
    let 순회결과 = 트리.중위순회()
    assert 순회결과 == [1, 3, 4, 5, 6, 7, 9]
    
    // 트리 속성 확인
    assert 트리.높이() == 4
    assert 트리.노드수() == 7
}

// 사용 예시
let mut 내트리 = 이진트리::Empty
[15, 10, 20, 8, 12, 16, 25].iter().for_each(|&| 내트리.삽입(값))

print("트리 중위 순회: {내트리.중위순회()}")
print("트리 높이: {내트리.높이()}")
print("노드 수: {내트리.노드수()}")
print("15 검색: {내트리.검색(&15)}")

⚡ 재귀 최적화 기법

꼬리 재귀 (Tail Recursion)

// 일반 재귀 vs 꼬리 재귀 비교

// 일반 재귀 - 스택 오버플로 위험
function 팩토리얼_일반(n: int) -> int {
    if n <= 1 {
        return 1
    }
    return n * 팩토리얼_일반(n - 1)  // 곱셈이 재귀 호출 후에 발생
}

// 꼬리 재귀 - 최적화 가능
function 팩토리얼_꼬리(n: int, 누적값: int = 1) -> int {
    if n <= 1 {
        return 누적값
    }
    return 팩토리얼_꼬리(n - 1, 누적값 * n)  // 재귀 호출이 마지막 작업
}

// 피보나치 꼬리 재귀
function 피보나치_꼬리(n: int, a: int = 0, b: int = 1) -> int {
    match n {
        case 0 => a
        case 1 => b
        case _ => 피보나치_꼬리(n - 1, b, a + b)
    }
}

// 리스트 뒤집기 꼬리 재귀
function 리스트뒤집기_꼬리<T>(
    원본: Array<T>,
    결과: Array<T> = [],
    인덱스: int = 0
) -> Array<T> {
    if 인덱스 >= 원본.length {
        return 결과
    }
    
    let mut 새결과 = [원본[인덱스]].to_vec()
    새결과.extend(결과)
    
    return 리스트뒤집기_꼬리(원본, 새결과, 인덱스 + 1)
}

// 거듭제곱 꼬리 재귀
function 거듭제곱_꼬리(밑: int, 지수: int, 누적값: int = 1) -> int {
    match 지수 {
        case 0 => 누적값
        case n if n % 2 == 0 => 거듭제곱_꼬리(밑 * 밑, n / 2, 누적값)
        case n => 거듭제곱_꼬리(밑, n - 1, 누적값 * 밑)
    }
} test {
    // 성능 비교를 위한 테스트
    assert 팩토리얼_일반(10) == 팩토리얼_꼬리(10)
    assert 피보나치_꼬리(10) == 55
    
    let 원본리스트 = [1, 2, 3, 4, 5]
    let 뒤집힌리스트 = 리스트뒤집기_꼬리(원본리스트)
    assert 뒤집힌리스트 == [5, 4, 3, 2, 1]
    
    assert 거듭제곱_꼬리(2, 10) == 1024
    assert 거듭제곱_꼬리(3, 4) == 81
}

print("꼬리 재귀 팩토리얼(10): {팩토리얼_꼬리(10)}")
print("꼬리 재귀 피보나치(15): {피보나치_꼬리(15)}")
print("거듭제곱 2^10: {거듭제곱_꼬리(2, 10)}")

메모이제이션 (Memoization)

use std::collections::HashMap

// 메모이제이션을 위한 래퍼 구조체
struct 메모이제이션<K, V>
where
    K: Clone + std::hash::Hash + Eq,
    V: Clone
{
    캐시: HashMap<K, V>,
    함수: fn(K, &mut 메모이제이션<K, V>) -> V
}

impl<K, V> 메모이제이션<K, V>
where
    K: Clone + std::hash::Hash + Eq,
    V: Clone
{
    function new(함수: fn(K, &mut 메모이제이션<K, V>) -> V) -> Self {
        return 메모이제이션 {
            캐시: HashMap::new(),
            함수
        }
    }
    
    function 호출(&mut self, 인자: K) -> V {
        match self.캐시.get(&인자) { case Some(결과) => return 결과.clone(), case None => {} }
        
        let 결과 = (self.함수)(인자.clone(), self)
        self.캐시.insert(인자, 결과.clone())
        return 결과
    }
}

// 메모이제이션을 사용한 피보나치
function 피보나치_메모_함수(n: int, 메모: &mut 메모이제이션<int, int>) -> int {
    match n {
        case 0 => 0
        case 1 => 1
        case _ => 메모.호출(n - 1) + 메모.호출(n - 2)
    }
}

// 조합 계산 (nCr) 메모이제이션
function 조합_메모_함수(
    매개변수: (int, int),
    메모: &mut 메모이제이션<(int, int), int>
) -> int {
    let (n, r) = 매개변수
    
    match (n, r) {
        case (_, 0) | (n, r) if n == r => 1
        case (n, r) if r > n => 0
        case (n, r) => {
            return 메모.호출((n - 1, r - 1)) + 메모.호출((n - 1, r))
        }
    }
}

// 최장 공통 부분 수열 (LCS) 메모이제이션
function LCS_메모_함수(
    매개변수: (string, string),
    메모: &mut 메모이제이션<(string, string), int>
) -> int {
    let (문자열1, 문자열2) = 매개변수
    
    if 문자열1.is_empty() || 문자열2.is_empty() {
        return 0
    }
    
    if 문자열1.chars().last() == 문자열2.chars().last() {
        let 앞부분1 = &문자열1[..문자열1.len() - 1]
        let 앞부분2 = &문자열2[..문자열2.len() - 1]
        return 1 + 메모.호출((앞부분1.to_string(), 앞부분2.to_string()))
    } else {
        let 앞부분1 = &문자열1[..문자열1.len() - 1]
        let 앞부분2 = &문자열2[..문자열2.len() - 1]
        let 경우1 = 메모.호출((문자열1.clone(), 앞부분2.to_string()))
        let 경우2 = 메모.호출((앞부분1.to_string(), 문자열2.clone()))
        return std::cmp::max(경우1, 경우2)
    }
} test {
    // 피보나치 메모이제이션 테스트
    let mut 피보나치메모 = 메모이제이션::new(피보나치_메모_함수)
    assert 피보나치메모.호출(10) == 55
    assert 피보나치메모.호출(20) == 6765
    
    // 조합 메모이제이션 테스트
    let mut 조합메모 = 메모이제이션::new(조합_메모_함수)
    assert 조합메모.호출((5, 2)) == 10
    assert 조합메모.호출((10, 3)) == 120
    
    // LCS 메모이제이션 테스트
    let mut LCS메모 = 메모이제이션::new(LCS_메모_함수)
    let 결과 = LCS메모.호출(("ABCDGH".to_string(), "AEDFHR".to_string()))
    assert 결과 == 3  // "ADH"
}

// 사용 예시
let mut 피보나치메모 = 메모이제이션::new(피보나치_메모_함수)
print("메모이제이션 피보나치(30): {피보나치메모.호출(30)}")

let mut 조합메모 = 메모이제이션::new(조합_메모_함수)
print("조합 C(20, 10): {조합메모.호출((20, 10))}")

🎯 실전 재귀 응용

백트래킹 (Backtracking)

// N-Queen 문제
function N퀸해결(n: int) -> Array<Array<int>> {
    let mut 해답들: Array<Array<int>> = []
    let mut 현재해답: Array<int> = vec![0; n]
    
    function 백트래킹(행: int) {
        if== n {
            해답들.push(현재해답.clone())
            return
        }
        
        forin 0..n {
            if 안전한위치(행, 열, &현재해답) {
                현재해답[행] =
                백트래킹(행 + 1)
                // 백트래킹: 자동으로 이전 상태로 복원
            }
        }
    }
    
    function 안전한위치(행: int, 열: int, 해답: &Array<int>) -> bool {
        for 이전행 in 0..행 {
            let 이전열 = 해답[이전행]
            
            // 같은 열 체크
            if 이전열 == 열 {
                return false
            }
            
            // 대각선 체크
            if (이전행 - 행).abs() == (이전열 as int -as int).abs() {
                return false
            }
        }
        return true
    }
    
    백트래킹(0)
    return 해답들
}

// 미로 탈출
struct 미로 {
    격자: Array<Array<bool>>,  // true = 길, false = 벽
    높이: int,
    너비: int
}

impl 미로 {
    function new(격자: Array<Array<bool>>) -> 미로 {
        let 높이 = 격자.len() as int
        let 너비 = if 높이 > 0 { 격자[0].len() as int } else { 0 }
        
        return 미로 { 격자, 높이, 너비 }
    }
    
    function 경로찾기(&self) -> Option<Array<(int, int)>> {
        let mut 방문함: Array<Array<bool>> = vec![vec![false; self.너비 as usize]; self.높이 as usize]
        let mut 경로: Array<(int, int)> = []
        
        if self.DFS(0, 0, &mut 방문함, &mut 경로) {
            return Some(경로)
        } else {
            return None
        }
    }
    
    function DFS(
        &self,
: int,
: int,
        방문함: &mut Array<Array<bool>>,
        경로: &mut Array<(int, int)>
    ) -> bool {
        // 경계 체크
        if< 0 ||>= self.높이 ||< 0 ||>= self.너비 {
            return false
        }
        
        // 벽이거나 이미 방문한 곳
        if !self.격자[행 as usize][열 as usize] || 방문함[행 as usize][열 as usize] {
            return false
        }
        
        // 현재 위치를 경로에 추가
        경로.push((행, 열))
        방문함[행 as usize][열 as usize] = true
        
        // 목표 지점 도달
        if== self.높이 - 1 &&== self.너비 - 1 {
            return true
        }
        
        // 4방향 탐색
        let 방향들 = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        for (dr, dc) in 방향들 {
            if self.DFS(행 + dr, 열 + dc, 방문함, 경로) {
                return true
            }
        }
        
        // 백트래킹: 경로에서 제거
        경로.pop()
        방문함[행 as usize][열 as usize] = false
        
        return false
    }
} test {
    // 4-Queen 문제 테스트
    let 해답들 = N퀸해결(4)
    assert 해답들.len() == 2  // 4x4에서는 2개의 해답이 존재
    
    // 미로 테스트
    let 미로격자 = [
        [true, false, true, true],
        [true, true, false, true],
        [false, true, true, true],
        [false, false, false, true]
    ]
    
    let 내미로 = 미로::new(미로격자.map(||.to_vec()).to_vec())
    let 경로 = 내미로.경로찾기()
    assert 경로.is_some()
}

// 사용 예시
let 해답들 = N퀸해결(8)  // 8-Queen 문제
print("8-Queen 해답 개수: {해답들.len()}")

match 해답들.first() { case Some(첫번째해답) => print("첫 번째 해답: {첫번째해답:?}"), case None => {} }

동적 계획법과 재귀

// 배낭 문제 (Knapsack Problem)
struct 물건 {
    무게: int,
    가치: int,
    이름: string
}

function 배낭문제_재귀(
    물건들: &Array<물건>,
    용량: int,
    인덱스: int = 0
) -> int {
    // 기저 조건
    if 인덱스 >= 물건들.len() || 용량 <= 0 {
        return 0
    }
    
    let 현재물건 = &물건들[인덱스]
    
    // 현재 물건을 넣을 수 없는 경우
    if 현재물건.무게 > 용량 {
        return 배낭문제_재귀(물건들, 용량, 인덱스 + 1)
    }
    
    // 두 가지 선택 중 최대값 선택
    let 넣는경우 = 현재물건.가치 + 배낭문제_재귀(
        물건들,
        용량 - 현재물건.무게,
        인덱스 + 1
    )
    let 안넣는경우 = 배낭문제_재귀(물건들, 용량, 인덱스 + 1)
    
    return std::cmp::max(넣는경우, 안넣는경우)
}

// 편집 거리 (Edit Distance)
function 편집거리_재귀(문자열1: &str, 문자열2: &str) -> int {
    if 문자열1.is_empty() {
        return 문자열2.len() as int
    }
    if 문자열2.is_empty() {
        return 문자열1.len() as int
    }
    
    let 문자1들: Vec<char> = 문자열1.chars().collect()
    let 문자2들: Vec<char> = 문자열2.chars().collect()
    
    if 문자1들.last() == 문자2들.last() {
        let 앞부분1: String = 문자1들[..문자1들.len()-1].iter().collect()
        let 앞부분2: String = 문자2들[..문자2들.len()-1].iter().collect()
        return 편집거리_재귀(&앞부분1, &앞부분2)
    }
    
    let 앞부분1: String = 문자1들[..문자1들.len()-1].iter().collect()
    let 앞부분2: String = 문자2들[..문자2들.len()-1].iter().collect()
    
    let 삽입 = 1 + 편집거리_재귀(문자열1, &앞부분2)
    let 삭제 = 1 + 편집거리_재귀(&앞부분1, 문자열2)
    let 교체 = 1 + 편집거리_재귀(&앞부분1, &앞부분2)
    
    return std::cmp::min(삽입, std::cmp::min(삭제, 교체))
}

// 계단 오르기 문제
function 계단오르기(n: int) -> int {
    match n {
        case 0 => 1
        case 1 => 1
        case 2 => 2
        case _ => 계단오르기(n - 1) + 계단오르기(n - 2)
    }
} test {
    // 배낭 문제 테스트
    let 물건들 = [
        물건 { 무게: 10, 가치: 60, 이름: "책".to_string() },
        물건 { 무게: 20, 가치: 100, 이름: "컴퓨터".to_string() },
        물건 { 무게: 30, 가치: 120, 이름: "텔레비전".to_string() }
    ]
    
    let 최대가치 = 배낭문제_재귀(&물건들, 50)
    assert 최대가치 == 220  // 책 + 컴퓨터 = 60 + 100 + 60 = 220
    
    // 편집 거리 테스트
    assert 편집거리_재귀("kitten", "sitting") == 3
    assert 편집거리_재귀("hello", "hallo") == 1
    
    // 계단 오르기 테스트
    assert 계단오르기(1) == 1
    assert 계단오르기(3) == 3
    assert 계단오르기(5) == 8
}

print("배낭 문제 최대 가치: {배낭문제_재귀(&물건들, 50)}")
print("편집 거리 (hello -> world): {편집거리_재귀("hello", "world")}")
print("10층 계단 오르는 방법: {계단오르기(10)}가지")

🚨 재귀 사용 시 주의사항

스택 오버플로 방지

// 위험한 재귀 - 스택 오버플로 가능성
function 위험한재귀(n: int) -> int {
    if n <= 0 {
        return 0
    }
    return 1 + 위험한재귀(n - 1)  // 큰 n에서 스택 오버플로
}

// 안전한 반복문 버전
function 안전한반복(n: int) -> int {
    let mut 결과 = 0
    for i in 1..=n {
        결과 += 1
    }
    return 결과
}

// 스택 오버플로 체크와 함께 재귀
function 안전한재귀(n: int, 깊이: int = 0) -> Result<int, string> {
    const 최대깊이: int = 1000
    
    if 깊이 > 최대깊이 {
        return Err("재귀 깊이 초과: 스택 오버플로 위험".to_string())
    }
    
    if n <= 0 {
        return Ok(0)
    }
    
    match 안전한재귀(n - 1, 깊이 + 1) {
        case Ok(결과) => Ok(1 + 결과)
        case Err(오류) => Err(오류)
    }
}

// 트램폴린 기법으로 스택 오버플로 방지
enum 트램폴린<T> {
    Done(T),
    Continue(Box<dyn FnOnce() -> 트램폴린<T>>)
}

function 트램폴린실행<T>(mut 결과: 트램폴린<T>) -> T {
    loop {
        match 결과 {
            case 트램폴린::Done(값) => return
            case 트램폴린::Continue(함수) => 결과 = 함수()
        }
    }
}

function 팩토리얼_트램폴린(n: int, 누적값: int = 1) -> 트램폴린<int> {
    if n <= 1 {
        return 트램폴린::Done(누적값)
    }
    
    return 트램폴린::Continue(Box::new(move || {
        팩토리얼_트램폴린(n - 1, 누적값 * n)
    }))
} test {
    // 안전한 재귀 테스트
    assert 안전한재귀(100).is_ok()
    assert 안전한재귀(2000).is_err()
    
    // 트램폴린 테스트
    let 결과 = 트램폴린실행(팩토리얼_트램폴린(10))
    assert 결과 == 3628800
}

print("안전한 재귀 (100): {안전한재귀(100).unwrap()}")
print("트램폴린 팩토리얼(10): {트램폴린실행(팩토리얼_트램폴린(10))}")

🎨 함수형 재귀 패턴

고차 함수와 재귀

// 재귀적 map 구현
function 재귀_map<T, U>(
    배열: Array<T>,
    변환함수: fn(T) -> U,
    인덱스: int = 0
) -> Array<U> {
    if 인덱스 >= 배열.len() {
        return []
    }
    
    let mut 결과 = [변환함수(배열[인덱스].clone())]
    결과.extend(재귀_map(배열, 변환함수, 인덱스 + 1))
    return 결과
}

// 재귀적 filter 구현
function 재귀_filter<T>(
    배열: Array<T>,
    조건함수: fn(&T) -> bool,
    인덱스: int = 0
) -> Array<T> {
    if 인덱스 >= 배열.len() {
        return []
    }
    
    let 현재요소 = &배열[인덱스]
    let 나머지결과 = 재귀_filter(배열.clone(), 조건함수, 인덱스 + 1)
    
    if 조건함수(현재요소) {
        let mut 결과 = [현재요소.clone()]
        결과.extend(나머지결과)
        return 결과
    } else {
        return 나머지결과
    }
}

// 재귀적 reduce 구현
function 재귀_reduce<T, U>(
    배열: Array<T>,
    초기값: U,
    축약함수: fn(U, T) -> U,
    인덱스: int = 0
) -> U {
    if 인덱스 >= 배열.len() {
        return 초기값
    }
    
    let 새초기값 = 축약함수(초기값, 배열[인덱스].clone())
    return 재귀_reduce(배열, 새초기값, 축약함수, 인덱스 + 1)
}

// Y 콤비네이터 구현
function Y<F, T>(함수: F) -> impl Fn(T) -> T
where
    F: Fn(&dyn Fn(T) -> T, T) -> T,
    T: 'static
{
    move |입력| {
        let 재귀함수 = |자기자신: &dyn Fn(T) -> T, 매개변수: T| -> T {
            함수(자기자신, 매개변수)
        }
        재귀함수(&재귀함수, 입력)
    }
} test {
    let 숫자들 = [1, 2, 3, 4, 5]
    
    // 재귀적 map 테스트
    let 제곱들 = 재귀_map(숫자들.clone(), |x| x * x)
    assert 제곱들 == [1, 4, 9, 16, 25]
    
    // 재귀적 filter 테스트
    let 짝수들 = 재귀_filter(숫자들.clone(), |&x| x % 2 == 0)
    assert 짝수들 == [2, 4]
    
    // 재귀적 reduce 테스트
    let 합계 = 재귀_reduce(숫자들.clone(), 0, |acc, x| acc + x)
    assert 합계 == 15
}

print("재귀 map (제곱): {재귀_map([1, 2, 3, 4], |x| x * x)}")
print("재귀 filter (짝수): {재귀_filter([1, 2, 3, 4, 5, 6], |&x| x % 2 == 0)}")
print("재귀 reduce (합계): {재귀_reduce([1, 2, 3, 4, 5], 0, |acc, x| acc + x)}")

🎯 재귀 마스터하기

재귀는 복잡한 문제를 우아하게 해결하는 강력한 도구입니다:

✅ 재귀를 사용하면 좋은 경우:

  • 트리/그래프 구조 처리
  • 분할 정복 알고리즘
  • 백트래킹 문제
  • 수학적 정의가 재귀적인 경우

⚠️ 재귀 사용 시 주의점:

  • 기저 조건 명확히 정의
  • 스택 오버플로 방지
  • 성능 최적화 고려 (메모이제이션, 꼬리 재귀)
  • 디버깅 어려움 고려

🚀 토파즈의 재귀 장점:

  • 패턴 매칭으로 명확한 조건 분리
  • 타입 안전성으로 오류 방지
  • 함수형 프로그래밍 패러다임 지원
  • 꼬리 재귀 최적화 자동 지원

재귀의 아름다운 세계를 토파즈와 함께 탐험해보세요! 🌀✨