En Rust, los closures son funciones anónimas que pueden capturar variables de su entorno. A diferencia de las funciones normales (fn), los closures no tienen un nombre fijo ni un tipo de dato explícito; el compilador genera un tipo único y opaco para cada closure basado en lo que captura. Esto introduce un problema fundamental cuando intentamos implementar recursión: la circularidad de la inferencia de tipos.

Para que un closure sea recursivo, su cuerpo debe llamar a “sí mismo”. Sin embargo, para llamar a un closure, el compilador necesita conocer su tipo exacto. Pero el tipo del closure depende de su contenido, y el contenido (la llamada recursiva) depende de conocer su propio tipo. Este ciclo de dependencias impide que el compilador resuelva el tipo del closure, resultando en un error de compilación.

Existen tres formas principales de abordar esto:
1. Usar funciones fn normales: Si no necesitas capturar variables del entorno, una función estándar es la solución más eficiente, ya que su tipo es concreto y conocido de antemano.
2. Indirección mediante Rc y Box: Se rompe la circularidad mediante el despacho dinámico (dyn Fn). Al envolver el closure en un Box o Rc, el compilador ya no necesita conocer el tipo único del closure, solo el tamaño del puntero al trait object.
3. El Y-Combinator: Un patrón funcional avanzado que permite inyectar la recursión sin necesidad de que el closure tenga un nombre o un tipo conocido de antemano, mediante la aplicación de una función de orden superior.

Es vital distinguir entre un fn item (un puntero a función con tipo concreto y tamaño cero que no captura nada) y un closure (que implementa el trait Fn, FnMut o FnOnce y puede capturar datos). Aunque un fn item puede ser tratado como un valor de primera clase, un closure solo puede convertirse en un fn item si no captura ningún entorno.

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

fn main() {
    // Definimos un alias de tipo para el closure para mejorar la legibilidad.
    // Usamos 'dyn Fn' para permitir el despacho dinámico (dynamic dispatch).
    type FactorialType = dyn Fn(u32) -> u32;

    // 1. El problema de la circularidad:
    // Un closure no puede llamarse a sí mismo directamente porque su tipo 
    // es una estructura anónima que el compilador no puede resolver 
    // si existe una dependencia circular en su definición.

    // 2. La solución: Indirección mediante Rc<RefCell<Option<...>>>
    // Usamos Rc para permitir la propiedad compartida entre el contenedor 
    // y el closure. Usamos RefCell para permitir la mutación inicial 
    // (insertar el closure una vez que el tipo sea conocido).
    // Usamos Option porque el contenedor debe existir antes de que el 
    // closure pueda ser creado.
    let container: Rc<RefCell<Option<Rc<FactorialType>>>> = Rc::new(RefCell::new(None));

    // Clonamos el Rc para que el closure sea dueño de una referencia al contenedor.
    let f_clone = Rc::clone(&container);

    // Definimos la lógica del closure de factorial.
    // En lugar de llamarse a sí mismo, busca la función en el contenedor.
    let logic = move |n: u32| -> u32 {
        if n == 0 {
            1
        } else {
            // Accedemos al closure almacenado mediante el RefCell y el Option.
            let f = f_clone.borrow().as_ref().expect("Error: Closure no inicializado");
            n * f(n - 1)
        }
    };

    // Ahora que el tipo de 'logic' ha sido inferido, podemos guardarlo en el contenedor.
    // Esto rompe el ciclo de dependencia de tipos del compilador.
    *container.borrow_mut() = Some(Rc::new(logic));

    // Extraemos el closure final para su uso.
    let factorial = container.borrow().as_ref().unwrap().clone();

    println!("Factorial de 5: {}", factorial(5));
    println!("Factorial de 0: {}", factorial(0));
}

Explicación del Código

  • type FactorialType = dyn Fn(u32) -> u32;: Definimos un trait object que representa cualquier tipo que implemente la firma de función deseada. Esto es clave para la indirección, ya que ocultamos el tipo único y desconocido del closure tras una interfaz conocida.
  • container: Rc<RefCell<Option<Rc<FactorialType>>>>: Esta es la estructura que resuelve el problema. El Rc permite que tanto el scope principal como el closure posean el contenedor. El RefCell permite la mutación interior necesaria para llenar el Option. El Option es vital para permitir la creación del objeto antes de que el closure mismo exista.
  • f_clone: Es un clon del Rc del contenedor. Al usar move en el closure logic, el closure captura este Rc, permitiéndole acceder al contenedor para encontrar su propia definición de forma recursiva.
  • logic: Es el closure que implementa la recursión. En la línea let f = f_clone.borrow().as_ref()..., el closure no se llama a sí mismo directamente, sino que pide al contenedor que le entregue una referencia a la función que se guardó en él. Esto es lo que rompe la dependencia circular de tipos para el compilador.
  • *container.borrow_mut() = Some(Rc::new(logic));: Aquí es donde se completa el ciclo. Una vez que la variable logic ha sido definida y su tipo ha sido determinado por el compilador, se inyecta en el contenedor, permitiendo que las llamadas recursivas dentro de logic funcionen correctamente.
  • factorial: Es la versión final del closure, extraída del contenedor mediante el despacho dinámico.

127