Fn, FnMut y FnOnce en Rust
En la página de Closures aprendiste a escribir una closure (clausura), cómo captura variables y cómo puede modificarlas. Esta página responde la siguiente pregunta: ¿cómo escribes tu propia función que reciba una closure? ¿Y cómo devuelves una closure desde una función?
La respuesta son tres traits (rasgos) pequeños: Fn, FnMut y FnOnce. Piensa en ellos como las normas de una biblioteca. Algunos visitantes solo leen los libros (Fn). Otros escriben notas en los márgenes (FnMut). Y otros se llevan un libro a casa y nunca lo devuelven (FnOnce). Rust mira lo que hace tu closure y le pone la etiqueta que le corresponde. Si al principio estos traits te dan miedo, tranquilo. Solo son tres, y el compilador elige el correcto por ti.
Por qué las closures necesitan traits
Sección titulada «Por qué las closures necesitan traits»Cada closure tiene su propio tipo único, inventado por el compilador. No puedes escribir el nombre de ese tipo. Incluso dos closures que se ven exactamente iguales tienen tipos distintos:
fn main() { let mut rules = vec![|price: u32| price * 2]; rules.push(|price: u32| price * 2);}error[E0308]: mismatched types --> src/main.rs:3:16 |3 | rules.push(|price: u32| price * 2); | ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure | = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait objectEl compilador dice que esperaba una closure y encontró otra distinta: no hay dos closures con el mismo tipo, aunque sean idénticas. Así que una función no puede decir “dame una closure del tipo X”. En su lugar dice “dame cualquier tipo que se pueda llamar de esta forma”. Eso es un trait bound (restricción de trait), igual que los de las páginas de Genéricos y Traits:
fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 { rule(price)}
fn main() { let coupon = 15; let half_price = |price: u32| price / 2; let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80)); println!("{}", price_after(use_coupon, 80));}Salida:
4065Lee F: Fn(u32) -> u32 así: “F es algún tipo que puedes llamar con un u32 y que devuelve un u32”. Las dos closures encajan, aunque sus tipos sean diferentes.
Los tres traits: Fn, FnMut y FnOnce
Sección titulada «Los tres traits: Fn, FnMut y FnOnce»El compilador mira qué hace el cuerpo de la closure con los valores que capturó:
Fn: solo los lee. Puedes llamarla todas las veces que quieras.FnMut: los modifica. Puedes llamarla muchas veces, pero necesitamut.FnOnce: los entrega (los mueve hacia fuera). Solo puedes llamarla una vez, porque después el valor ya no está.
fn main() { let shop = String::from("Panadería La Esquina");
// Fn: solo lee `shop` let greet = || println!("¡Bienvenido a {shop}!"); greet(); greet();
// FnMut: modifica `orders` let mut orders = 0; let mut take_order = || { orders += 1; println!("pedido n.º {orders} recibido"); }; take_order(); take_order();
// FnOnce: le devuelve `receipt` a quien la llama let receipt = String::from("2 cruasanes, 1 café"); let hand_over = || receipt; let customer_copy = hand_over(); println!("el cliente tiene: {customer_copy}");}Salida:
¡Bienvenido a Panadería La Esquina!¡Bienvenido a Panadería La Esquina!pedido n.º 1 recibidopedido n.º 2 recibidoel cliente tiene: 2 cruasanes, 1 caféhand_over devuelve el propio receipt, no una copia. Una vez entregado el recibo, a la closure ya no le queda nada que dar. Por eso es FnOnce.
Esta es la imagen que conviene tener en la cabeza. Imagina tres círculos, uno dentro de otro:
- El círculo grande de fuera es
FnOnce. Cualquier closure se puede llamar al menos una vez, así que todas las closures están aquí. - Dentro hay un círculo más pequeño,
FnMut: las closures que se pueden llamar una y otra vez. - Justo en el centro está
Fn: las closures que se pueden llamar una y otra vez sin cambiar nada.
Así que toda closure Fn también es FnMut y FnOnce. Esto importa cuando escribes una función. Un parámetro que pide FnOnce acepta cualquier closure. Un parámetro que pide Fn es el más exigente.
Algo que sorprende a mucha gente: la palabra clave move no decide el trait. move || println!("{shop}") se queda con el ownership (propiedad) de shop, pero solo lo lee, así que sigue siendo Fn. Lo único que decide es el cuerpo.
Escribir funciones que reciben closures
Sección titulada «Escribir funciones que reciben closures»Hay tres formas de escribir la misma restricción. Todas significan lo mismo, así que elige la que se lea mejor:
// 1. Un parámetro genérico con una restricciónfn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x)}
// 2. `impl Fn` en la posición del argumento: más corto, sin nombrefn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(f(x))}
// 3. Una cláusula `where`: más ordenada cuando la restricción es largafn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>where F: Fn(i32) -> i32,{ scores.iter().map(|&s| f(s)).collect()}
fn main() { let bonus = 10; let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50)); println!("{}", apply_twice(add_bonus, 50)); println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));}Salida:
6070[50, 75, 100]Quizá te preguntes cómo pudimos pasar add_bonus tres veces. Una closure que solo lee lo que captura es barata de copiar, así que Rust la copia cada vez.
Ahora FnMut y FnOnce. Con FnMut, el parámetro necesita mut, porque llamar a la closure cambia lo que lleva dentro. Con FnOnce, la puedes llamar una vez y se acabó:
fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) { for task in tasks { on_done(task); }}
fn on_shutdown<F: FnOnce()>(cleanup: F) { println!("apagando..."); cleanup();}
fn main() { let mut finished = Vec::new(); process_tasks(&["enviar correos", "respaldar la base de datos"], |task| { finished.push(task.to_string()); }); println!("terminadas: {:?}", finished);
on_shutdown(|| { // Este bucle se queda con el ownership de `finished`, así que la closure es FnOnce for task in finished { println!("archivada: {task}"); } });}Salida:
terminadas: ["enviar correos", "respaldar la base de datos"]apagando...archivada: enviar correosarchivada: respaldar la base de datosUna buena regla práctica: pide el trait menos exigente con el que tu función pueda funcionar. Si llamas a la closure una sola vez, pide FnOnce, y quien llame podrá pasar cualquier cosa. Lo verás también en la biblioteca estándar: Option::map recibe FnOnce, mientras que Iterator::map recibe FnMut.
Cuando eliges la restricción equivocada
Sección titulada «Cuando eliges la restricción equivocada»El compilador detecta cualquier desajuste, y aquí sus mensajes son muy amables. Supón que una función pide Fn, pero tu closure modifica un contador:
fn run_report<F: Fn()>(f: F) { f(); f();}
fn main() { let mut runs = 0; run_report(|| { runs += 1; println!("informe ejecutado {runs}"); });}error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure --> src/main.rs:9:9 |1 | fn run_report<F: Fn()>(f: F) { | - change this to accept `FnMut` instead of `Fn`...8 | run_report(|| { | ---------- -- in this closure | | | expects `Fn` instead of `FnMut`9 | runs += 1; | ^^^^^^^^^ cannot assignEl mensaje dice que no se puede asignar a runs porque es una variable capturada en una closure Fn. Incluso te da la solución: aceptar FnMut y añadir mut al parámetro. Con ese cambio, el programa imprime informe ejecutado 1 e informe ejecutado 2:
fn run_report<F: FnMut()>(mut f: F) { f(); f();}El otro error clásico es llamar dos veces a una closure FnOnce:
fn main() { let report = String::from("ventas del T3"); let send = || { let sent = report; println!("enviando {sent}"); }; send(); send();}error[E0382]: use of moved value: `send` --> src/main.rs:8:5 |7 | send(); | ------ `send` moved due to this call8 | send(); | ^^^^ value used here after move |note: closure cannot be invoked more than once because it moves the variable `report` out of its environment --> src/main.rs:4:20 |4 | let sent = report; | ^^^^^^La nota lo dice claramente: la closure no se puede llamar más de una vez porque saca report de su entorno. La primera llamada entregó report, así que no queda nada para una segunda. Si de verdad necesitas dos llamadas, toma prestado en su lugar (let sent = &report;) y la closure pasa a ser Fn. Si le pasas una closure así a una función que pide Fn, obtienes un mensaje parecido: error[E0507]: cannot move out of report, a captured variable in an Fn closure.
Devolver closures con impl Fn
Sección titulada «Devolver closures con impl Fn»Una función también puede construir una closure y devolverla. El tipo de retorno se escribe impl Fn(i32) -> i32, que significa “algún tipo de closure; no necesitas saber cómo se llama”:
fn make_adder(amount: i32) -> impl Fn(i32) -> i32 { move |x| x + amount}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 { move |x| x * factor}
fn main() { let add_tip = make_adder(5); let triple = make_multiplier(3);
println!("{}", add_tip(20)); println!("{}", triple(7)); println!("{}", add_tip(triple(10)));}Salida:
252135¿Por qué el move? Sin él, la closure solo tomaría prestado amount. Pero amount pertenece a make_adder y desaparece en cuanto make_adder termina. La closure se quedaría con una referencia a la nada. Rust se niega:
error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function --> src/main.rs:2:5 |2 | |x| x + amount | ^^^ ------ `amount` is borrowed here | | | may outlive borrowed value `amount` |help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword |2 | move |x| x + amount | ++++El error avisa de que la closure podría vivir más que la función, pero toma prestado amount, que pertenece a esa función. La ayuda propone justo move. Con move, la closure se lleva su propia copia de amount y la carga consigo, como llevarte tu almuerzo en vez de dejarlo en la nevera de otra persona.
Closures en una caja: muchas closures, un solo tipo
Sección titulada «Closures en una caja: muchas closures, un solo tipo»impl Fn representa un único tipo oculto. Así que no puedes devolver una closure desde la rama if y otra distinta desde la rama else. Tampoco puedes meter closures diferentes en el mismo Vec. Para las dos cosas, mete cada closure en una caja: Box<dyn Fn(f64) -> f64> significa “cualquier closure con esta forma, guardada en el heap”. Es la misma idea que en la página de Trait objects.
Aquí tienes un carrito de compra que aplica, en orden, una lista de reglas de precio con nombre:
fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> { if express { Box::new(move |total| total + fee * 2.0) } else { Box::new(move |total| total + fee) }}
fn main() { let member_percent = 10.0; let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![ ("descuento de socio", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))), ("cupón", Box::new(move |total: f64| total - coupon)), ("envío estándar", shipping_rule(false, 3.5)), ];
let mut total = 80.0; println!("inicio: {total:.2}"); for (name, rule) in &rules { total = rule(total); println!("tras {name}: {total:.2}"); }}Salida:
inicio: 80.00tras descuento de socio: 72.00tras cupón: 67.00tras envío estándar: 70.50Meter la closure en una caja cuesta una pequeña reserva de memoria en el heap, y cada llamada pasa por un puntero. Casi nunca es un problema. Usa impl Fn cuando hay un solo tipo de closure, y Box<dyn Fn> cuando hay varios.
Guardar una closure en un struct
Sección titulada «Guardar una closure en un struct»Un struct (estructura) también puede guardar una closure. Como no puedes nombrar el tipo de la closure, haces el struct genérico sobre ella. Aquí tienes un pequeño botón que ejecuta código cuando lo pulsan:
struct Button<F: Fn()> { label: String, on_click: F,}
impl<F: Fn()> Button<F> { fn click(&self) { println!("[{}] pulsado", self.label); (self.on_click)(); }}
fn main() { let user = String::from("Ada"); let save = Button { label: String::from("Guardar"), on_click: move || println!("guardando el borrador de {user}"), };
save.click(); save.click();}Salida:
[Guardar] pulsadoguardando el borrador de Ada[Guardar] pulsadoguardando el borrador de AdaFíjate en los paréntesis de (self.on_click)(). Le dicen a Rust “toma el campo y luego llámalo”. Si el botón necesita modificar lo que capturó en cada clic, usa F: FnMut() y haz que el método reciba &mut self.
Punteros a función: fn(i32) -> i32
Sección titulada «Punteros a función: fn(i32) -> i32»Las funciones normales también se pueden pasar de un lado a otro. Su tipo se escribe con fn en minúscula, como fn(i32) -> i32. Es un puntero a función, simplemente la dirección de un trozo de código. Una closure que no captura nada también puede convertirse en uno:
fn double(x: i32) -> i32 { x * 2}
fn square(x: i32) -> i32 { x * x}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 { op(x)}
fn main() { println!("{}", apply_op(double, 4)); println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1]; let mut value = 3; for step in steps { value = step(value); } println!("{value}");}Salida:
8337Todos los punteros a función comparten un mismo tipo, así que puedes meterlos en un array sin cajas. La pega es que una closure que captura algo no puede convertirse en puntero a función. El compilador lo dice así: closures can only be coerced to fn types if they do not capture any variables (las closures solo se pueden convertir en tipos fn si no capturan ninguna variable). La buena noticia va en el otro sentido: toda función normal también implementa Fn, FnMut y FnOnce, así que puedes pasar double a la función apply de antes. Por eso la mayoría del código usa los traits de closure: aceptan las dos cosas.
Errores comunes
Sección titulada «Errores comunes»Olvidar mut en un parámetro FnMut. Llamar a una FnMut la modifica, así que su variable tiene que ser mut:
error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable --> src/main.rs:3:9 |3 | on_done(task); | ^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) { | +++El compilador indica que no puede tomar on_done como mutable porque no se declaró como mutable, y te muestra dónde añadir mut.
Devolver closures distintas desde -> impl Fn. Dos closures son dos tipos, aunque se parezcan:
fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 { if express { move |total| total + fee * 2.0 } else { move |total| total + fee }}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:5:9 |2 | / if express {3 | | move |total| total + fee * 2.0 | | ------------------------------ | | | | | the expected closure | | expected because of this4 | | } else {5 | | move |total| total + fee | | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure6 | | } | |_____- `if` and `else` have incompatible types | = note: expected closure `{closure@src/main.rs:3:9: 3:21}` found closure `{closure@src/main.rs:5:9: 5:21}` = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait objecthelp: you could change the return type to be a boxed trait object |1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> { |help: if you change the return type to expect trait objects, box the returned expressions |3 ~ Box::new(move |total| total + fee * 2.0)4 | } else {5 ~ Box::new(move |total| total + fee) |Haz lo que propone el compilador: devuelve Box<dyn Fn(f64) -> f64> y envuelve cada closure en Box::new(...).
Llamar a un campo closure como si fuera un método. Si escribes self.on_click(), Rust busca un método llamado on_click:
error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope --> src/main.rs:9:14 |9 | self.on_click(); | ^^^^^^^^ field, not a method |help: to call the type parameter stored in `on_click`, surround the field access with parentheses |9 | (self.on_click)(); | + +El mensaje dice que on_click es un campo, no un método, y que para llamarlo hay que rodear el acceso al campo con paréntesis.
Práctica
Sección titulada «Práctica»1. Un dispensador de turnos
Sección titulada «1. Un dispensador de turnos»Escribe una función ticket_dispenser(start: u32) que devuelva una closure. Cada llamada a la closure da el siguiente número de turno: start, luego start + 1, y así sucesivamente. Crea dos dispensadores (una panadería que empieza en 100 y una farmacia que empieza en 1) y demuestra que cuentan por separado. Pista: la closure modifica su propio contador, así que el tipo de retorno es impl FnMut() -> u32, y vas a necesitar move.
Ver solución
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 { let mut next = start; move || { let ticket = next; next += 1; ticket }}
fn main() { let mut bakery = ticket_dispenser(100); let mut pharmacy = ticket_dispenser(1);
println!("panadería: {}", bakery()); println!("panadería: {}", bakery()); println!("farmacia: {}", pharmacy()); println!("panadería: {}", bakery());}Salida:
panadería: 100panadería: 101farmacia: 1panadería: 102Cada dispensador movió su propio next hacia dentro, así que nunca comparten contador. Las variables bakery y pharmacy necesitan mut porque son FnMut.
2. Una función para reintentar
Sección titulada «2. Una función para reintentar»Escribe retry(max_attempts, task). Llama a task hasta que devuelva Ok, o hasta haberlo intentado max_attempts veces. Imprime una línea por cada intento fallido. Pruébala con una closure que falla dos veces con "servidor ocupado" y después funciona. Pista: la closure de prueba cuenta sus propias llamadas, así que la restricción es FnMut() -> Result<String, String>, y el parámetro necesita mut.
Ver solución
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>where F: FnMut() -> Result<String, String>,{ for attempt in 1..=max_attempts { match task() { Ok(value) => return Ok(value), Err(e) => println!("el intento {attempt} falló: {e}"), } } Err(String::from("me rindo"))}
fn main() { let mut calls = 0; let result = retry(5, || { calls += 1; if calls < 3 { Err(String::from("servidor ocupado")) } else { Ok(String::from("datos descargados")) } }); println!("{:?} tras {calls} llamadas", result);}Salida:
el intento 1 falló: servidor ocupadoel intento 2 falló: servidor ocupadoOk("datos descargados") tras 3 llamadasretry llama a la tarea más de una vez, así que FnOnce no sirve, y la tarea modifica calls, así que Fn tampoco. FnMut es justo lo que hace falta. Cuando retry termina, la closure ya no se usa, así que main puede volver a leer calls.
Resumen
Sección titulada «Resumen»- Cada closure tiene su propio tipo único, así que las funciones aceptan closures mediante una restricción de trait como
F: Fn(i32) -> i32. Fnsolo lee lo que captura,FnMutlo modifica, yFnOncelo mueve hacia fuera y solo se puede llamar una vez. Lo decide el cuerpo, nomove.- Toda
Fntambién esFnMutyFnOnce. Pide el trait menos exigente que necesite tu función. - Escribe las restricciones como
<F: Fn(..)>,impl Fn(..)o en una cláusulawhere. Los parámetrosFnMutnecesitanmut. - Devuelve una closure con
-> impl Fn(..)(normalmente conmove). UsaBox<dyn Fn(..)>para closures distintas en cada rama o dentro de unVec. fn(i32) -> i32es un puntero a función. Encajan en él las funciones normales y las closures que no capturan nada.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. Una closure hace total += price sobre una variable capturada. ¿Cuál es el trait más estricto que implementa?
2. Tu función llama a su parámetro closure exactamente una vez. ¿Qué restricción acepta más closures?
3. ¿Por qué make_adder(amount: i32) -> impl Fn(i32) -> i32 necesita move |x| x + amount?
4. Quieres un Vec con varias closures de descuento diferentes. ¿De qué tipo tienen que ser los elementos?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Moving Captured Values Out of Closures and the Fn Traits — El libro de Rust (en inglés): la explicación oficial de cómo elige el compilador entre
Fn,FnMutyFnOnce. Fn,FnMutyFnOnce— documentación de la biblioteca estándar (en inglés): la definición de cada trait, con ejemplos cortos de cuándo usarlo.- Input parameters — Rust by Example (en inglés): ejemplos ejecutables de funciones que reciben closures con cada una de las tres restricciones.
- Output parameters — Rust by Example (en inglés): cómo devolver closures con
impl Fn, y por qué hace faltamove. - Closure types — The Rust Reference (en inglés): las reglas exactas de cómo capturan valores las closures y qué traits implementan.