Avatar ·

Можно ли создать рекурсивное замыкание в Rust?

Всем привет! Я учу Rust и пытаюсь написать рекурсивное замыкание для вычисления факториала. Вот мой код: ```rust let factorial = |n: u32| -> u32 { if n <= 1 { 1 } else { n * factorial(n - 1) // ошибка: cannot capture `factorial` in its own initializer } }; ``` Компилятор выдаёт ошибку: "cannot capture `factorial` in its own initializer". Я понимаю, что замыкание не может захватить переменную, которая ещё не полностью инициализирована. Но как тогда сделать рекурсивное замыкание? Я пробовал объявить `factorial` как `let factorial: Box u32>`, но это не помогло. Знаю, что можно использовать обычную функцию `fn factorial(n: u32) -> u32`, но мне нужно именно замыкание, потому что я хочу захватить внешние переменные. Подскажите, как правильно организовать рекурсию в замыкании? Желательно с примером кода, который работает. Заранее спасибо! <<>>

Проблема рекурсивных замыканий в Rust

Действительно, напрямую создать рекурсивное замыкание нельзя из-за особенностей системы владения и замыканий. Замыкание не может ссылаться на себя до завершения инициализации. Однако есть несколько проверенных подходов.

Решение 1: Использование Rc и RefCell (или Cell)

Самый гибкий способ — обернуть замыкание в умный указатель, который позволяет разделять владение:

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let factorial: Rc<RefCell<Box<dyn Fn(u32) -> u32>>> = 
        Rc::new(RefCell::new(Box::new(|_| 0))); // временная заглушка

    let factorial_clone = factorial.clone();
    *factorial.borrow_mut() = Box::new(move |n: u32| -> u32 {
        if n <= 1 {
            1
        } else {
            n * factorial_clone.borrow()(n - 1)
        }
    });

    println!("5! = {}", factorial.borrow()(5)); // 120
}

Решение 2: Использование Cell для простых случаев

Если замыкание не захватывает внешние переменные с нетривиальным владением, можно использовать Cell:

use std::cell::Cell;

fn main() {
    let factorial: Cell<Option<Box<dyn Fn(u32) -> u32>>> = Cell::new(None);
    
    let fact = |n: u32| -> u32 {
        let f = factorial.take().unwrap();
        let result = f(n);
        factorial.set(Some(f)); // возвращаем обратно
        result
    };
    
    let real_fact: Box<dyn Fn(u32) -> u32> = Box::new(move |n| {
        if n <= 1 { 1 } else { n * fact(n - 1) }
    });
    
    factorial.set(Some(real_fact));
    println!("5! = {}", factorial.take().unwrap()(5)); // 120
}

Решение 3: Рекурсия через трейт (более идиоматично)

Лучший способ — определить собственный трейт для рекурсивных замыканий:

trait RecursiveFn {
    fn call(&self, n: u32) -> u32;
}

impl<F> RecursiveFn for F where F: Fn(u32, &dyn RecursiveFn) -> u32 {
    fn call(&self, n: u32) -> u32 {
        (self)(n, self)
    }
}

fn main() {
    let factorial = |n: u32, f: &dyn RecursiveFn| -> u32 {
        if n <= 1 { 1 } else { n * f.call(n - 1) }
    };
    
    println!("5! = {}", factorial.call(5)); // 120
}

Важные замечания

  • Производительность: Использование Rc и RefCell добавляет накладные расходы. Для критичного кода лучше использовать обычные функции.
  • Захват переменных: Решение с Rc/RefCell позволяет замыканию захватывать внешние переменные через move.
  • Альтернатива: Если рекурсия глубокая, рассмотрите итеративный подход или использование fn с замыканием через аргумент.

Рекомендую третий подход (с трейтом) — он наиболее элегантный и не требует изменяемой памяти. Но для быстрого прототипирования подойдёт первый вариант.

<<>>

Альтернативный подход: Использование fn с захватом через аргументы

Можно обойти ограничение замыканий, передавая рекурсивный вызов как аргумент:

fn main() {
    let factorial = |n: u32| {
        fn inner(n: u32, f: &dyn Fn(u32) -> u32) -> u32 {
            if n <= 1 { 1 } else { n * f(n - 1) }
        }
        // Используем Y-комбинатор вручную
        let rec = |n| inner(n, &|n| inner(n, &|n| inner(n, &|n| inner(n, &|n| n))));
        rec(n)
    };
    
    println!("5! = {}", factorial(5)); // 120
}

Этот метод не требует Rc или RefCell, но ограничен глубиной рекурсии (нужно вручную дублировать замыкание). Для глубокой рекурсии используйте Box:

fn main() {
    let factorial: Box<dyn Fn(u32) -> u32> = Box::new(|n| {
        if n <= 1 { 1 } else { n * factorial(n - 1) }
    });
    // Ошибка! factorial ещё не инициализировано.
    // Исправление: используйте unsafe или Cell.
}

На практике для рекурсии в Rust предпочитают обычные функции или итеративные алгоритмы. Замыкания с рекурсией — редкий случай, обычно оправданный только при захвате сложного окружения.