Styl funkcyjny w Rust: niemutowalność, deklaratywność i obsługa błędów
Programowanie funkcyjne w Rust eliminuje typowe problemy stylów imperatywnych i obiektowych: mutowalność, pętle imperatywne i niejawną obsługę błędów. Zamiast mutowania stanów, kod opiera się na niemutowalnych transformacjach, operacjach deklaratywnych i jawnych typach jak Result i Option. To sprawia, że programy są przewidywalne i mniej podatne na błędy.
Rust domyślnie jest niemutowalny, co zmusza programistów do przemyślenia znanych wzorców. Styl funkcyjny wykorzystuje iteratory, kompozycję funkcji i dopasowanie wzorców do czystego kodu bez efektów ubocznych.
Niemutowalność zamiast mutacji
Mutowalne zmienne tworzą ukryte zależności: wartość zmienia się w nieoczekiwanych miejscach, co utrudnia debugowanie. W kodzie imperatywnym zmienna mut x może się zmienić w dowolnym momencie między deklaracją a użyciem.
fn my_function(num: i32) -> i32 {
let mut x = num;
// ...jeszcze 30 linii...
x += 96;
// ...jeszcze 50 linii...
x
}
Tutaj oryginalna wartość jest psuta, a wynik nieprzewidywalny z powodu możliwych zmian pośrednich. Niemutowalność rozwiązuje to: tworzone są nowe wartości zamiast zmiany starych.
let x = 10;
let new_x = process(x); // x pozostaje niezmieniony
W Rust niemutowalność jest normą. Do mutacji wymagane jest jawne mut, ale styl funkcyjny minimalizuje jego użycie:
- Sprawdź, czy zadanie można rozwiązać niemutowalnie za pomocą iteratorów lub map.
- Jeśli mutacja jest nieunikniona, izoluj ją w minimalnym bloku.
- Preferuj
clonelub borrowing do rozpowszechniania danych.
To zmniejsza ryzyko race conditions i upraszcza równoległość.
Deklaratywność przeciwko imperatywności
Kod imperatywny opisuje kroki wykonania: pętle, warunki, mutacje. Funkcyjny — pożądany wynik. Aby uzyskać kwadraty liczb od 0 do 9, wariant imperatywny wymaga wektora i push:
let mut squares = Vec::new();
for num in 0..10 {
squares.push(num * num);
}
Deklaratywny używa łańcucha iteratorów:
let squares: Vec<i32> = (0..10)
.map(|x| x * x)
.collect();
Zalety stylu deklaratywnego:
- Mniej kodu: łańcuchy iteratorów zastępują pętle.
- Niemutowalność: brak mutacji kolekcji.
- Komponowalność: funkcje łatwo łączyć.
- Równoległość:
.par_iter()dla wielowątkowości bez lock'ów.
Iteratory w Rust są leniwe: map nie wykonuje się aż do collect lub for_each, co optymalizuje pamięć.
Jawna obsługa błędów
Wyjatki w innych językach maskują błędy: try-catch pozwala ignorować typy i kontekst. W Rust Result<T, E> i ? wymagają jawnej obsługi lub przekazania.
Zły przykład z unwrap:
old_toaster().unwrap(); // Panika przy błędzie
Prawidłowe podejście używa match lub map_err:
let toast = match old_toaster() {
Ok(t) => t,
Err(e) => {
eprintln!("Błąd: {}", e);
std::process::exit(1);
}
};
Skróty do obsługi:
matchdla pełnej kontroli.map_errdo transformacji błędów.?tylko jeśli wywołujący obsłuży.- Biblioteki
anyhowlubthiserrordla wygody.
To zapobiega panikom w produkcji i zmusza do myślenia o błędach na etapie kompilacji.
Eliminacja niejawnej pustości
W językach imperatywnych null ukrywa się wszędzie, powodując NullPointerException. Rust używa Option<T>: Some(value) lub None jawnie.
let maybe_candies: Option<Vec<Candy>> = get_candies();
let candies = match maybe_candies {
Some(c) => c,
None => Vec::new(),
};
Dopasowanie wzorców gwarantuje obsługę obu przypadków. Kompilator nie pominie None.
Co jest ważne
- Niemutowalność domyślnie zmniejsza błędy od stanów.
- Iteratory i map zastępują pętle, czyniąc kod deklaratywnym.
- Result i Option zmuszają do jawnej obsługi błędów i pustości.
- Kompozycja funkcji upraszcza testowanie i refaktoryzację.
- Styl funkcyjny pasuje do 80% zadań w Rust; mutacje — tylko w IO lub wydajnych hotspotach.
— Editorial Team
Brak komentarzy.