Hogere-orde functies zijn functies die andere functies als parameters accepteren of ze als resultaat retourneren. Rust heeft van meet af aan de nadruk gelegd op typeveiligheid en prestaties, wat zich vertaalt in het werken met dit soort functies.
Geschiedenis van de kwestie:
In functionele talen worden hogere-orde functies als standaard beschouwd, maar in veel systeem talen leiden ze vaak tot prestatieverlies (bijvoorbeeld door allocaties of het onvermogen om code "in-line" te zetten). In Rust is deze functionaliteit geïmplementeerd via een strikt type-systeem, statische dispatch, of traits (Fn, FnMut, FnOnce), wat in de meeste gevallen extra overhead voorkomt.
Probleem:
Het belangrijkste probleem is de noodzaak om een functie of closure door te geven, terwijl je de typeveiligheid behoudt, de mogelijkheid om variabelen vast te leggen (de eenvoud van lambda-expressies) en prestaties zonder allocaties of virtuele aanroepen.
Oplossing:
In Rust zijn hogere-orde functies geïmplementeerd via generieke parameters en trait wrappers voor functies/closures. De standaard traits Fn, FnMut en FnOnce stellen je in staat om heel duidelijk de vereisten voor de doorgegeven functie te declareren (kan deze muteren of het omringende milieu consumeren). Doorpassing via generics maakt inlining van aanroepen op compileertijd mogelijk. Er is ook dynamische dispatch via Box<dyn Fn...>, wanneer het type niet van tevoren bekend is.
Codevoorbeeld:
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]
Belangrijkste kenmerken:
Wat is het verschil tussen Fn, FnMut en FnOnce?
Velen denken dat ze alleen in syntaxis verschillen of dat Fn en FnMut alles wederzijds inzetbaar kunnen doen. In werkelijkheid:
FnOnce kan slechts één keer worden aangeroepen (bijvoorbeeld als de lambda een vastgelegde waarde naar binnen verplaatst).FnMut kan de toestand van het vastgelegde milieu wijzigen, maar kan meerdere keren worden aangeroepen.Fn wijzigt het milieu niet.Voorbeeld:
let mut sum = 0; let mut add = |x| { sum += x; }; // add implementeert FnMut, maar niet Fn
Kan een functie als waarde worden doorgegeven zonder boxing?
Vaak denken mensen dat alle functie-argumenten noodzakelijkerwijs boxed moeten zijn (Box<dyn Fn...>). In werkelijkheid is boxing SLECHTS vereist voor dynamische dispatch, wanneer het type nummer niet bekend is totdat het uitgevoerd wordt. Via generieke parameters kan de functie volledig statisch getypeerd zijn, zonder allocaties en boxing.
Wanneer verliest een closure zijn Copy?
Sommigen denken dat eenvoudige closures altijd Copy of Clone zijn, zolang de variabele binnen Copy is. In werkelijkheid zijn closures standaard niet Copy, zelfs niet als de vastgelegde variabelen Copy zijn. Je moet expliciet de trait implementeren of het met eenvoudige functies afhandelen.
In het project werd alleen Box<dyn Fn()> gebruikt voor alle callbacks in collecties, zonder na te denken over inlining en allocaties. Het resultaat was dat er geen prestatieverbetering kon worden bereikt, frequente allocaties leidde tot vertragingen.
Voordelen:
Nadelen:
Evenement handlers werden ingesteld via generieke functies met trait-beperkingen FnMut, volledig zonder allocaties.
Voordelen:
Nadelen: