FnOnce, FnMut y Fn: los tres traits del closure
Cada closure implementa uno o varios de tres traits según lo que haga con lo capturado: `FnOnce` consume el entorno, `FnMut` lo muta, `Fn` solo lo lee. Cómo se relacionan por herencia, cómo el compilador decide cuáles implementa un closure, y por qué al pedir un closure conviene exigir el trait más débil posible.
Un closure no implementa un trait único: implementa el conjunto de tres —FnOnce, FnMut, Fn— que su comportamiento le permite. La distinción no es burocrática. Marca cuántas veces se puede llamar, si al llamarlo cambia su estado interno, y si puede correr en paralelo. Y no la eliges tú: la deduce el compilador mirando qué hace el cuerpo con lo que capturó. Un closure que solo lee es tan flexible como se puede ser; uno que consume lo capturado, tan rígido como que solo sirve una vez. Aprender a leer esos tres traits es aprender a leer el contrato de cualquier función de orden superior en Rust.
- Conocer los tres traits
FnOnce,FnMutyFny el receptor (self,&mut self,&self) de cada uno. - Entender la jerarquía de subtraits: todo
FnesFnMut, y todoFnMutesFnOnce. - Deducir cuáles implementa un closure según lea, mute o consuma sus capturas.
- Elegir, al aceptar un closure, el bound más permisivo que la tarea permita.
Tres traits, tres receptores
Los tres traits describen tres maneras de invocar un valor llamable, y se diferencian en cómo toman self:
// Firmas conceptuales de la biblioteca estandar:
trait FnOnce<Args> {
type Output;
fn call_once(self, args: Args) -> Self::Output; // consume el closure
}
trait FnMut<Args>: FnOnce<Args> {
fn call_mut(&mut self, args: Args) -> Self::Output; // presta mutable
}
trait Fn<Args>: FnMut<Args> {
fn call(&self, args: Args) -> Self::Output; // presta compartido
}
Léelos por el receptor, que lo dice todo:
FnOncetomaselfpor valor: llamarlo consume el closure, así que solo se garantiza una invocación. Es lo que permite mover fuera lo capturado.FnMuttoma&mut self: puede llamarse muchas veces, y cada llamada puede mutar el estado capturado.Fntoma&self: puede llamarse muchas veces desde referencias compartidas y solo lee lo capturado; nunca lo altera.
FnOnce: consume
Recibe self. Se garantiza una sola llamada. Es el único que puede mover fuera lo capturado. Todos los closures lo implementan.
FnMut: muta
Recibe &mut self. Muchas llamadas, cada una puede alterar el estado capturado. Exige acceso exclusivo mientras se ejecuta.
Fn: lee
Recibe &self. Muchas llamadas desde referencias compartidas, sin mutar nada. El único apto para compartirse entre hilos sin más.
La jerarquía: quien puede más, puede menos
Fíjate en los : de las firmas: Fn es subtrait de FnMut, que a su vez es subtrait de FnOnce. Esto codifica una relación de inclusión:
Todo closure que implementa
Fnimplementa tambiénFnMutyFnOnce. TodoFnMutimplementaFnOnce. La flecha va de lo más capaz a lo más general.
Tiene sentido operacional: si puedes llamar algo a través de &self (Fn), también puedes a través de &mut self (FnMut, porque de un &mut sacas un &) y también consumiéndolo una vez (FnOnce). Lo contrario no vale: un closure que consume lo capturado no se puede llamar dos veces, así que no es FnMut ni Fn.
flowchart TB fn[Fn con and self solo lee] -->|implica| fnmut[FnMut con and mut self muta] fnmut -->|implica| fnonce[FnOnce con self consume] fn -.->|se puede llamar N veces| usos1[Muchas invocaciones] fnonce -.->|se puede llamar 1 vez| usos2[Una sola invocacion] style fn fill:#a6e3a1,color:#11111b style fnmut fill:#89b4fa,color:#11111b style fnonce fill:#fab387,color:#11111b
Cómo el compilador decide
No hay palabra clave que asigne un trait. El compilador clasifica el closure por lo que su cuerpo hace con las capturas, siguiendo la misma lógica de captura de la lección anterior:
let saludo = String::from("hola");
// Solo LEE saludo -> implementa Fn, FnMut y FnOnce
let leer = || println!("{saludo}");
// MUTA saludo -> implementa FnMut y FnOnce (no Fn)
let mut anadir = || saludo.push('!');
// CONSUME saludo -> implementa solo FnOnce
let consumir = || { let s = saludo; s.len() };
La regla es directa:
- Si el cuerpo solo lee las capturas → el closure es
Fn(y por herenciaFnMutyFnOnce). - Si muta alguna captura → es
FnMut(yFnOnce), pero noFn. - Si mueve fuera alguna captura, o requiere posesión → es
FnOncey nada más.
El closure obtiene siempre el trait más capaz que su cuerpo justifique. Uno que solo lee cae en la cúspide (Fn); uno que consume, en el suelo (FnOnce).
No solo los closures implementan estos traits. Una fn nombrada y un closure sin capturas implementan los tres —no tienen entorno que consumir ni mutar—, así que puedes pasar i32::abs o |x| x + 1 a cualquier parámetro con bound Fn. Además, un closure sin capturas coacciona a un puntero a función fn(i32) -> i32, un tipo con nombre y de tamaño una palabra, útil en FFI o en tablas de despacho. En cuanto un closure captura algo, deja de ser un simple puntero y se vuelve la struct anónima de siempre, invocable solo a través de los traits Fn.
Si un closure implementa solo FnOnce, el compilador lo sabe y te impide invocarlo dos veces: la primera llamada lo consumió. let g = || { let s = saludo; s }; g(); g(); falla con “value moved”. El sistema de tipos convierte “esta función solo sirve una vez” en un error de compilación, no en un fallo en ejecución.
Del otro lado: qué trait exigir
Cuando escribes una función que recibe un closure, eliges el bound. Y aquí la jerarquía se invierte en intuición: el trait más débil —FnOnce— es el que más closures acepta, porque todos lo implementan. El más fuerte —Fn— es el más exigente.
// La llamo una sola vez: pido FnOnce, acepto cualquier closure.
fn una_vez<F: FnOnce() -> String>(f: F) -> String { f() }
// La llamo repetidamente y toleran mutacion: pido FnMut.
fn repite<F: FnMut()>(mut f: F) { f(); f(); f(); }
// Necesito llamarla desde &self, quiza en paralelo: pido Fn.
fn comparte<F: Fn(i32) -> i32>(f: &F) -> i32 { f(1) + f(2) }
La regla de diseño: pide el trait más permisivo que tu uso tolere. ¿La llamas una vez? FnOnce. ¿Muchas veces y no te importa que mute? FnMut. ¿Necesitas invocarla solo con &self —para compartirla entre hilos o llamarla de forma reentrante—? Solo entonces Fn. Exigir Fn cuando bastaba FnOnce rechaza sin motivo closures perfectamente válidos.
Que existan tres traits en vez de uno es una de esas decisiones donde Rust convierte una cuestión de recursos en una cuestión de tipos. En un lenguaje con recolector de basura, “llamar a una función” es una operación única y uniforme: la función es un puntero al heap, la invocas cuantas veces quieras, y si captura algo mutable, allá tú con las carreras de datos. Rust descompone ese acto único en tres según lo que le hace al entorno capturado, y lo hace codificando la diferencia en el receptor de un método —self, &mut self, &self—, que no es un detalle sino la pieza central. Tomar self por valor significa consumir: por eso FnOnce solo promete una llamada, porque después de mover el closure entero ya no queda closure. Tomar &mut self significa acceso exclusivo temporal: por eso FnMut puede mutar sus capturas pero exige que nadie más lo toque mientras se ejecuta. Tomar &self significa acceso compartido de solo lectura: por eso Fn, y solo Fn, se puede invocar desde varias referencias a la vez, lo que lo vuelve el único de los tres apto para compartirse entre hilos sin más ceremonia. La jerarquía de subtraits no es taxonomía académica: es la afirmación de que poder acceder de la forma más restringida implica poder acceder de las más laxas, la misma lógica que gobierna las referencias en todo el lenguaje. Y como el compilador deriva el trait del cuerpo en vez de pedírtelo, el closure siempre acaba tan flexible como honestamente puede ser: ni te obliga a prometer inmutabilidad que no cumples, ni te niega la reutilización cuando de verdad no mutas nada. En una función de orden superior, el bound Fn, FnMut o FnOnce es, leído así, una declaración precisa de cómo piensas usar el argumento —cuántas veces, con qué acceso, con qué posibilidad de compartir— y el compilador se encarga de que ese contrato no se rompa jamás. Tres receptores, tres teorías del uso, un solo sistema de tipos que las hace cumplir.
- Escribe tres closures sobre una misma variable capturada: uno que la lea, uno que la mute y uno que la consuma; anota qué traits implementa cada uno.
- Toma el closure que solo consume e intenta llamarlo dos veces; lee el error y relaciónalo con el receptor
selfdecall_once. - Escribe
fn repite<F: FnMut()>(mut f: F)que llame aftres veces, y pásale un closure que incremente un contador capturado. - Cambia el bound del punto 3 de
FnMutaFny observa qué closures dejan de ser aceptados y por qué. - Razona por qué
FnOncees el bound que más closures admite pese a ser el trait “más débil” de los tres.