Funkcje wyższego rzędu to funkcje, które przyjmują inne funkcje jako argumenty lub zwracają je jako wynik. Rust od samego początku swojego rozwoju kładł nacisk na bezpieczeństwo typów i wydajność, co znajduje odzwierciedlenie w pracy z tymi rodzajami funkcji.
Historia zagadnienia:
W językach funkcyjnych funkcje wyższego rzędu są standardem, jednak w wielu językach systemowych często prowadziły do utraty wydajności (na przykład, z powodu alokacji lub niemożności „inline’owania” kodu). W Rust funkcjonalność ta została zrealizowana poprzez ścisły system typów, statyczną dyspozycję lub traity (Fn, FnMut, FnOnce), co pozwala w większości przypadków uniknąć nadmiernych kosztów.
Problem:
Podstawowy problem polega na konieczności przekazania funkcji lub zamknięcia, zachowując bezpieczeństwo typów, możliwość uchwycenia zmiennych (łatwość użycia wyrażeń lambda) oraz wydajność bez alokacji lub wywołań wirtualnych.
Rozwiązanie:
W Rust funkcje wyższego rzędu są realizowane za pomocą parametrów generycznych i opakowań traitowych dla funkcji/zamknięć. Standardowe traity Fn, FnMut i FnOnce pozwalają na bardzo jasne określenie wymagań dotyczących przekazywanej funkcji (czy może zmieniać stan lub konsumować otoczenie). Przekazywanie przez generiki pozwala na inline’owanie wywołań na etapie kompilacji. Istnieje także dynamiczna dyspozycja poprzez Box<dyn Fn...>, gdy typ nie jest znany wcześniej.
Przykład kodu:
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]
Kluczowe cechy:
Czym różnią się Fn, FnMut i FnOnce?
Wielu uważa, że różnią się tylko składnią lub że Fn i FnMut mogą robić wszystko zamiennie. W rzeczywistości:
FnOnce może być wywołany tylko raz (na przykład, jeśli lambda przenosi uchwycony element do wnętrza).FnMut może zmieniać stan uchwyconego otoczenia, ale może być wywoływany wiele razy.Fn nie zmienia otoczenia.Przykład:
let mut sum = 0; let mut add = |x| { sum += x; }; // add realizuje FnMut, ale nie Fn
Czy można przekazać funkcję jako wartość bez boxing?
Często myśli się, że wszelkie argumenty funkcyjne muszą być boxed (Box<dyn Fn...>). W rzeczywistości boxing jest potrzebny TYLKO dla dynamicznej dyspozycji, gdy typ nie jest znany przed wykonaniem. Dzięki parametrom generycznym funkcja może być w pełni statycznie typowana, bez alokacji i boxa.
W jakim przypadku zamknięcie przestaje być Copy?
Niektórzy sądzą, że proste zamknięcie zawsze jest Copy lub Clone, jeśli zmienne wewnętrzne są Copy. W rzeczywistości zamknięcia domyślnie nie są Copy, nawet jeśli uchwycone zmienne są Copy. Należy jawnie zaimplementować trait lub ograniczyć się do prostych funkcji.
W projekcie stosowano tylko Box<dyn Fn()> dla wszystkich callbacków w kolekcjach, nie zastanawiając się nad inline’owaniem i alokacjami. W rezultacie nie udało się uzyskać wzrostu wydajności, częste alokacje prowadziły do opóźnień.
Zalety:
Wady:
Obsługiwacze zdarzeń były konfigurowane przez funkcje generyczne z ograniczeniem traitu FnMut, całkowicie bez alokacji.
Zalety:
Wady: