Powrót do strony głównej

Funkcjonalny Rust: niezmienność i iteratory

Artykuł wyjaśnia zalety stylu funkcyjnego w Rust: niezmienność zamiast mutacji, deklaratywne iteratory zamiast pętli, jawna obsługa błędów z Result i Option. Podane są przykłady kodu i rekomendacje dla middle/senior deweloperów.

Czysty kod w Rust: funkcjonalne podejście bez mutacji
Advertisement 728x90

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.

Google AdInline article slot
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 clone lub borrowing do rozpowszechniania danych.

To zmniejsza ryzyko race conditions i upraszcza równoległość.

Google AdInline article slot

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:

Google AdInline article slot
  • 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:

  • match dla pełnej kontroli.
  • map_err do transformacji błędów.
  • ? tylko jeśli wywołujący obsłuży.
  • Biblioteki anyhow lub thiserror dla 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

Advertisement 728x90

Czytaj dalej