Les fonctions d'ordre supérieur sont des fonctions qui acceptent d'autres fonctions comme paramètres ou les retournent comme résultats. Rust a toujours mis l'accent sur la sécurité des types et la performance, ce qui se reflète dans son traitement des fonctions de ce type.
Historique de la question :
Dans les langages fonctionnels, les fonctions d'ordre supérieur sont considérées comme la norme, mais dans de nombreux langages système, elles entraînent souvent des fuites de performance (par exemple, à cause des allocations ou de l'impossibilité d'« inline » le code). En Rust, cette fonctionnalité est réalisée à travers un système typé strict, une dispatch statique ou des traits (Fn, FnMut, FnOnce), ce qui permet d'éviter dans la plupart des cas des surcoûts.
Problème :
Le problème principal réside dans la nécessité de passer une fonction ou une fermeture tout en préservant la sécurité des types, la capacité de capturer des variables (facilité des expressions lambda) et la performance sans allocations ni appels virtuels.
Solution :
En Rust, les fonctions d'ordre supérieur sont mises en œuvre à travers des paramètres génériques et des wrappers de traits pour les fonctions/fermements. Les traits standards Fn, FnMut et FnOnce permettent de déclarer très clairement les exigences relatives à la fonction passée (peut-elle muter ou consommer l'environnement). Le passage par des génériques permet d'inliner les appels au moment de la compilation. Il existe également une dispatch dynamique via Box<dyn Fn...>, lorsque le type est inconnu à l'avance.
Exemple de code :
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]
Caractéristiques clés :
Quelles sont les différences entre Fn, FnMut et FnOnce ?
Beaucoup pensent qu'elles diffèrent seulement par la syntaxe ou que Fn et FnMut peuvent faire tout de manière interchangeable. En réalité :
FnOnce ne peut être appelé qu'une seule fois (par exemple, si la fermeture déplace une valeur capturée à l'intérieur).FnMut peut modifier l'état de l'environnement capturé, mais peut être appelé plusieurs fois.Fn ne modifie pas l'environnement.Exemple :
let mut sum = 0; let mut add = |x| { sum += x; }; // add implémente FnMut, mais pas Fn
Peut-on passer une fonction comme valeur sans boxing ?
On pense souvent que tous les arguments de fonctions doivent nécessairement être boxed (Box<dyn Fn...>). En réalité, le boxing est requis UNIQUEMENT pour la dispatch dynamique, lorsque le numéro de type est inconnu avant l'exécution. Grâce aux paramètres génériques, la fonction peut être entièrement typée de manière statique, sans allocations et sans box.
Dans quel cas une fermeture cesse-t-elle d'être Copy ?
Certains pensent qu'une simple fermeture est toujours Copy ou Clone si la variable à l'intérieur est Copy. En réalité, les fermetures ne sont pas Copy par défaut, même si les variables capturées le sont. Il est nécessaire d'implémenter explicitement le trait ou de se contenter de fonctions simples.
Dans un projet, uniquement Box<dyn Fn()> était utilisé pour tous les callbacks dans les collections, sans réfléchir à l'inlining et aux allocations. En conséquence, aucune augmentation de la performance n'a été obtenue, des allocations fréquentes entraînant des latences.
Avantages :
Inconvénients :
Les gestionnaires d'événements étaient configurés via des fonctions génériques avec la contrainte de trait FnMut, sans recourir aux allocations.
Avantages :
Inconvénients :