La historia de la pregunta proviene de la decisión de Rust de implementar cierres como abstracciones de costo cero a través de estructuras anónimas en lugar de objetos de función recolectados por basura. A diferencia de lenguajes como JavaScript o Python, Rust debe codificar las reglas de propiedad, préstamo y mutabilidad directamente en el tipo del cierre. Los tres rasgos—Fn, FnMut y FnOnce—forman una estricta jerarquía basada en el parámetro self en sus métodos call, permitiendo al compilador verificar en tiempo de compilación que el uso de un cierre respeta los invariantes de seguridad de memoria de su entorno capturado.
El problema se centra en la distinción entre cómo un cierre captura variables (por referencia o por valor a través de move) y cómo las utiliza internamente. FnOnce requiere self (consumiendo propiedad), permitiendo que el cierre mueva variables capturadas fuera de su entorno pero restringiéndolo a una única invocación. FnMut requiere &mut self, permitiendo la mutación del estado capturado pero exigiendo acceso único al cierre. Fn requiere &self, habilitando múltiples invocaciones concurrentes pero prohibiendo la mutación de las variables capturadas a menos que se utilice mutabilidad interior. Un cierre que mueve un tipo no Copy en su cuerpo se convierte en FnOnce porque la primera invocación dejaría el entorno en un estado movido, invalidando las llamadas subsiguientes. Los candidatos a menudo confunden la palabra clave move—que simplemente obliga a capturar por valor—con el rasgo FnOnce, sin reconocer que un cierre move que contiene solo tipos Copy aún implementa Fn.
La solución implica seleccionar el rasgo de limitación menos restrictivo necesario para la API. Si el cierre se invoca exactamente una vez, use FnOnce para aceptar la más amplia variedad de cierres (incluyendo aquellos que consumen su entorno). Si se requieren múltiples invocaciones con mutación, use FnMut. Para acceso de solo lectura concurrente o repetido, use Fn. El compilador deriva automáticamente estas implementaciones basadas en el análisis de captura, sin requerir implementación manual de rasgos.
fn apply_once<F: FnOnce()>(f: F) { f(); } fn apply_mut<F: FnMut()>(mut f: F) { f(); f(); } fn apply_fn<F: Fn()>(f: F) { f(); f(); } let data = vec![1, 2, 3]; let consume = move || drop(data); // FnOnce: Vec no es Copy apply_once(consume); let mut count = 0; let mut increment = || { count += 1; }; // FnMut: muta la captura apply_mut(&mut increment); let value = 42; let print = move || println!("{}", value); // Fn: i32 es Copy apply_fn(print); apply_fn(print); // Válido: print es Fn
Considere un programador de tareas asíncronas en un servidor web de alto rendimiento que acepta ganchos definidos por el usuario para procesar solicitudes entrantes. La API del programador inicialmente requería que todos los ganchos implementaran Fn para permitir la ejecución potencial paralela.
Descripción del problema: Una nueva característica requería que los ganchos mantuvieran estadísticas por conexión, necesitando la mutación de contadores capturados. Los desarrolladores intentaron pasar cierres move que capturaban variables mut counter, pero el compilador rechazó esto porque Fn requiere &self, que no puede mutar campos mut poseídos sin mutabilidad interior. El equipo se enfrentó a la elección entre relajar la restricción del rasgo o reestructurar la firma del gancho.
Solución 1: Mutabilidad interior con tipos atómicos:
Reemplace el contador u64 con AtomicU64 y captúrelo a través de Arc. El cierre implementa Fn porque la mutación ocurre a través de operaciones atómicas en &self, sin requerir acceso mutable al cierre mismo.
Pros: Mantiene la restricción de Fn, permite al programador ejecutar ganchos concurrentemente desde múltiples hilos sin sincronización en el cierre mismo.
Contras: Introduce sobrecargas atómicas a nivel de hardware y complejidad de ordenación de memoria. Requiere asignación de Arc incluso para uso de un solo hilo, destruyendo los principios de abstracción de costo cero para contadores simples.
Solución 2: Límite FnMut con ejecución secuencial:
Cambie la API del programador para aceptar cierres FnMut. El programador almacena ganchos en un Vec<Box<dyn FnMut()>> y los invoca secuencialmente mientras mantiene acceso &mut.
Pros: Cero sobrecarga en tiempo de ejecución para la mutación. Garantía en tiempo de compilación de que no ocurran condiciones de carrera, ya que el sistema de tipos impone acceso único durante la invocación.
Contras: Previene la invocación concurrente del mismo gancho y complica el almacenamiento interno del programador (requiere &mut self en el propio programador). Rompe la compatibilidad con los ganchos existentes de Fn a menos que se utilicen implementaciones generales.
Solución elegida: Se seleccionó la Solución 2 (FnMut) porque la arquitectura del servidor procesó conexiones por hilo, eliminando la necesidad de ejecución concurrente del gancho. El equipo prefirió la seguridad en tiempo de compilación en lugar de la flexibilidad de los ganchos concurrentes, aceptando el cambio de API como una evolución correcta aunque disruptiva.
Resultado: El programador manejó con éxito ganchos que mantienen estado sin sobrecarga en tiempo de ejecución. El sistema de tipos evitó un error sutil donde dos hilos podrían haber incrementado concurrentemente un contador no atómico, lo que habría sido posible si se hubiese utilizado RefCell con Fn sin la debida sincronización.
¿Hace que la palabra clave move en la definición de un cierre haga automáticamente que ese cierre implemente FnOnce en lugar de Fn o FnMut?
No. La palabra clave move dicta solo que las variables capturadas se muevan al entorno del cierre por valor, en lugar de ser prestadas. La implementación del rasgo depende únicamente de cómo el cuerpo del cierre utiliza sus capturas. Si el cierre mueve un tipo no Copy fuera de su entorno (consumiéndolo), implementa FnOnce. Si solo muta las capturas, implementa FnMut. Si solo lee o utiliza tipos Copy por valor, implementa Fn, incluso con la palabra clave move. Por ejemplo, let x = 5; let f = move || x + 1; implementa Fn porque i32 es Copy.
¿Por qué se puede llamar a una función que acepta FnOnce con un cierre que implementa Fn, pero no viceversa?
Fn es un subtipo de FnMut, que es un subtipo de FnOnce. Esto significa que cada cierre que implementa Fn implementa automáticamente FnMut y FnOnce, pero lo inverso no es cierto. Un parámetro de función delimitado por FnOnce acepta cualquier cierre que pueda ser llamado una vez, que incluye aquellos que pueden ser llamados múltiples veces (Fn y FnMut). Por otro lado, una función que requiere Fn exige que el cierre soporte invocación a través de una referencia compartida (&self), la cual no pueden satisfacer los cierres que consumen su entorno (FnOnce únicamente). Esto sigue el estándar de subtipado: un tipo más capaz (Fn) puede ser usado donde se requiere uno menos capaz (FnOnce).