Las funciones de orden superior son aquellas que toman otras funciones como parámetros o las devuelven como resultado. Desde sus inicios, Rust ha puesto un énfasis en la seguridad de tipos y el rendimiento, lo que se refleja en el trabajo con funciones de este tipo.
Historia de la cuestión:
En los lenguajes funcionales, las funciones de orden superior se consideran estándar, sin embargo, en muchos lenguajes de sistemas a menudo conducen a fugas de rendimiento (por ejemplo, debido a asignaciones o incapacidad para "inlinar" el código). En Rust, esta funcionalidad se implementa a través de un estricto sistema de tipos, despacho estático o rasgos (Fn, FnMut, FnOnce), lo que permite evitar sobrecostos en la mayoría de los casos.
Problema:
El problema principal radica en la necesidad de pasar una función o una clausura, manteniendo la seguridad de tipos, la capacidad de capturar variables (la facilidad de las expresiones lambda) y el rendimiento sin asignaciones o llamadas virtuales.
Solución:
En Rust, las funciones de orden superior se implementan a través de parámetros genéricos y envolturas de rasgos para funciones/clausuras. Los rasgos estándar Fn, FnMut y FnOnce permiten declarar de manera muy clara los requisitos para la función que se pasa (si puede mutar o consumir el entorno). Pasar a través de genéricos permite inlinar llamadas en el tiempo de compilación. También existe despacho dinámico a través de Box<dyn Fn...>, cuando el tipo no se conoce de antemano.
Ejemplo de código:
fn apply_to_vec<F: Fn(i32) -> i32>(v: Vec<i32>, f: F) -> Vec<i32> { v.into_iter().map(f).collect() } let nums = vec![1, 2, 3]; let doubled = apply_to_vec(nums, |x| x * 2); // doubled == [2, 4, 6]
Características clave:
¿Cuál es la diferencia entre Fn, FnMut y FnOnce?
Muchos piensan que solo difieren en la sintaxis o que Fn y FnMut pueden hacer todo intercambiablemente. De hecho:
FnOnce solo se puede llamar una vez (por ejemplo, si la lambda mueve el valor capturado dentro).FnMut puede cambiar el estado del entorno capturado, pero se puede llamar varias veces.Fn no cambia el entorno.Ejemplo:
let mut sum = 0; let mut add = |x| { sum += x; }; // add implementa FnMut pero no Fn
¿Se puede pasar una función como valor sin boxing?
A menudo se piensa que cualquier función argumento debe ser obligatoriamente boxed (Box<dyn Fn...>). En realidad, el boxing solo es necesario para el despacho dinámico, cuando el número de tipo no se conoce hasta la ejecución. A través de parámetros genéricos, la función puede estar completamente tipificada estáticamente, sin asignaciones y sin boxing.
¿En qué caso una clausura deja de ser Copy?
Algunos creen que una clausura simple siempre es Copy o Clone si la variable dentro es Copy. En realidad, las clausuras no son Copy por defecto, incluso si las variables capturadas son Copy. Es necesario implementar explícitamente el rasgo o usar funciones simples.
En el proyecto, solo se utilizó Box<dyn Fn()> para todos los callbacks en las colecciones, sin pensar en inlining y asignaciones. Como resultado, no se logró un aumento en el rendimiento, las frecuentes asignaciones llevaban a retrasos.
Ventajas:
Desventajas:
Los manejadores de eventos se configuraron a través de funciones genéricas con restricciones de rasgo FnMut, sin necesidad de asignaciones.
Ventajas:
Desventajas: