Closures en Rust: ¿magia o matemática estricta?
\n\nSi alguna vez has escrito algo como |x| x + 1 en Rust, has usado closures. Pero en cuanto intentas pasar ese closure a una función o guardarlo en una estructura, el compilador de repente empieza a exigir: «¿Qué trait? ¿Fn, FnMut o FnOnce?».
Y aquí muchos se quedan perplejos. Especialmente si vienes de JavaScript, Python o Ruby, donde los closures son «simplemente funciones que capturan variables». En Rust es más complicado (e interesante).
\n\nHoy entenderemos qué sucede realmente bajo el capó cuando Rust decide qué trait asignar a tu closure. No es magia: es un sistema de tipos estricto que te protege de condiciones de carrera y punteros colgantes. ¡Allá vamos!
\n\nResumen rápido: los tres traits de closures
\n\nEn Rust hay tres traits que los closures implementan:
\n\n- \n
FnOnce— puede ser invocado una sola vez. Toma posesión de las variables capturadas. \n FnMut— puede ser invocado múltiples veces, pero puede modificar las variables capturadas (toma&mut). \n Fn— puede ser invocado múltiples veces y no modifica las variables capturadas (toma&). \n
Cada closure implementa automáticamente todos los traits aplicables. Es decir, si un closure implementa Fn, también implementa automáticamente FnMut y FnOnce. Pero al revés no funciona.
¿Por qué? Porque Fn es el más restrictivo, y FnOnce el más débil. La jerarquía de herencia es la siguiente:
FnOnce ← FnMut ← Fn\n\nEs decir, FnMut: FnOnce, y Fn: FnMut. Recuerda esta escalera: es clave.
¿Cómo decide Rust qué trait asignar a un closure?
\n\nEl compilador observa cómo exactamente el closure usa las variables capturadas. Hay tres modos de captura:
\n\n- \n
- Por referencia (
&T) — el closure solo lee la variable. \n - Por referencia mutable (
&mut T) — el closure modifica la variable. \n - Por valor (
T) — el closure toma posesión de la variable. \n
Así es como esto afecta a los traits:
\n\n| Modo de captura | Traits implementados | Ejemplo |
|---|---|---|
Solo &T | Fn + FnMut + FnOnce | || println!("{}", x) |
&mut T (pero no T) | FnMut + FnOnce | || x += 1 |
T (posesión) | Solo FnOnce | || drop(x) o move || x |
Analicemos cada caso con código real.
\n\nEjemplo 1: Solo lectura → Fn
\n\nlet x = 42;\nlet read_closure = || println!("x = {}", x);\n\n// Se puede invocar tantas veces como se quiera\nread_closure();\nread_closure();\n\n// E incluso pasarlo a una función que requiera Fn\nfn call_fn<F: Fn()>(f: F) {\n f();\n}\ncall_fn(read_closure);\n\nAquí el closure simplemente lee x. No necesita modificarlo ni tomar posesión. Rust entiende: «Ok, esto es seguro — le damos Fn».
Ejemplo 2: Modificación → FnMut
\n\nlet mut x = 42;\nlet mut mut_closure = || {\n x += 1;\n println!("x = {}", x);\n};\n\nmut_closure(); // x = 43\nmut_closure(); // x = 44\n\n// Lo pasamos a una función que requiera FnMut\nfn call_fn_mut<F: FnMut()>(mut f: F) {\n f();\n f();\n}\ncall_fn_mut(mut_closure);\n\nObserva que declaramos mut_closure como let mut. ¿Por qué? Porque invocar FnMut requiere una referencia mutable