Ir al contenido

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.

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 object

El 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:

40
65
▶ Pruébalo tú mismo

Lee 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.

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 necesita mut.
  • 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 recibido
pedido n.º 2 recibido
el cliente tiene: 2 cruasanes, 1 café
▶ Pruébalo tú mismo

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.

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ón
fn 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 nombre
fn 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 larga
fn 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:

60
70
[50, 75, 100]
▶ Pruébalo tú mismo

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 correos
archivada: respaldar la base de datos
▶ Pruébalo tú mismo

Una 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.

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 assign

El 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 call
8 | 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.

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:

25
21
35
▶ Pruébalo tú mismo

¿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.00
tras descuento de socio: 72.00
tras cupón: 67.00
tras envío estándar: 70.50
▶ Pruébalo tú mismo

Meter 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.

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] pulsado
guardando el borrador de Ada
[Guardar] pulsado
guardando el borrador de Ada
▶ Pruébalo tú mismo

Fí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.

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:

8
3
37
▶ Pruébalo tú mismo

Todos 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.

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 this
4 | | } else {
5 | | move |total| total + fee
| | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `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 object
help: 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.

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: 100
panadería: 101
farmacia: 1
panadería: 102
▶ Pruébalo tú mismo

Cada dispensador movió su propio next hacia dentro, así que nunca comparten contador. Las variables bakery y pharmacy necesitan mut porque son FnMut.

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 ocupado
el intento 2 falló: servidor ocupado
Ok("datos descargados") tras 3 llamadas
▶ Pruébalo tú mismo

retry 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.

  • Cada closure tiene su propio tipo único, así que las funciones aceptan closures mediante una restricción de trait como F: Fn(i32) -> i32.
  • Fn solo lee lo que captura, FnMut lo modifica, y FnOnce lo mueve hacia fuera y solo se puede llamar una vez. Lo decide el cuerpo, no move.
  • Toda Fn también es FnMut y FnOnce. Pide el trait menos exigente que necesite tu función.
  • Escribe las restricciones como <F: Fn(..)>, impl Fn(..) o en una cláusula where. Los parámetros FnMut necesitan mut.
  • Devuelve una closure con -> impl Fn(..) (normalmente con move). Usa Box<dyn Fn(..)> para closures distintas en cada rama o dentro de un Vec.
  • fn(i32) -> i32 es un puntero a función. Encajan en él las funciones normales y las closures que no capturan nada.

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