Функциональное программирование в Rust: продвинутые техники и идиомы

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в Rust: продвинутый уровень

Rust — это мультипарадигмальный язык, который органично сочетает императивный, объектно-ориентированный и функциональный стили. На продвинутом уровне функциональное программирование в Rust позволяет писать более безопасный, выразительный и лаконичный код. В этой статье мы разберем ключевые концепции: замыкания с захватом окружения, продвинутые итераторы, функторы и монады на примере Option и Result, а также каррирование и композицию функций.

1. Замыкания: владение, заимствование и мутабельность

Замыкания в Rust — это не просто анонимные функции. Они могут захватывать переменные из окружающей области видимости тремя способами: по ссылке (&T), по мутабельной ссылке (&mut T) и по значению (T). Компилятор автоматически выбирает наименее ограничивающий трейт: Fn, FnMut или FnOnce.

Рассмотрим пример с захватом по значению и использованием move:

fn main() {
    let data = vec![1, 2, 3];
    
    // Захват по значению (move) — владение передается замыканию
    let closure = move || {
        println!("{:?}", data);
        // data больше недоступна в основной функции
    };
    
    closure();
    // println!("{:?}", data); // Ошибка: data перемещена
}

Продвинутый прием — использование замыканий для создания фабрик функций:

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    move |x| x * factor
}

fn main() {
    let double = make_multiplier(2);
    let triple = make_multiplier(3);
    
    println!("{}", double(5));  // 10
    println!("{}", triple(5));  // 15
}

Здесь impl Fn(i32) -> i32 — это возвращаемый тип-замыкание. Rust автоматически генерирует анонимный тип, реализующий трейт Fn.

2. Продвинутые итераторы: цепочки, адаптеры и ленивые вычисления

Итераторы в Rust — это мощный инструмент функционального стиля. Они ленивы: вычисления происходят только при вызове терминального метода (collect, for_each, fold и т.д.).

Рассмотрим сложный конвейер обработки данных:

fn process_data(data: Vec<i32>) -> Vec<i32> {
    data.iter()
        .filter(|&x| x > 0)                     // Оставляем положительные
        .map(|x| x * 2)                          // Удваиваем
        .enumerate()                             // Добавляем индексы
        .filter(|(i, _)| i % 2 == 0)             // Четные индексы
        .map(|(_, x)| x)                         // Извлекаем значение
        .collect()                               // Терминальный метод
}

Продвинутый прием — использование flat_map для работы с вложенными структурами и fold для аккумуляции:

fn main() {
    let nested = vec![vec![1, 2], vec![3, 4, 5], vec![6]];
    
    let flattened: Vec<i32> = nested.iter()
        .flat_map(|v| v.iter())
        .copied()
        .collect();
    
    println!("{:?}", flattened); // [1, 2, 3, 4, 5, 6]
    
    // fold — функциональный аналог reduce
    let sum = flattened.iter()
        .fold(0, |acc, x| acc + x);
    
    println!("Сумма: {}", sum); // 21
}

Важно помнить: цепочки итераторов не создают промежуточных коллекций, если не вызывать collect. Это экономит память и повышает производительность.

3. Монады Option и Result: функциональная обработка ошибок

В функциональном программировании Option и Result — это монады. Они позволяют цепочечно обрабатывать опциональные значения и ошибки без вложенных match или if let.

Рассмотрим типичный сценарий с вложенными проверками:

fn find_user(id: u32) -> Option<String> {
    if id == 1 { Some("Alice".to_string()) } else { None }
}

fn find_email(name: &str) -> Option<String> {
    if name == "Alice" { Some("alice@example.com".to_string()) } else { None }
}

fn main() {
    // Императивный подход (громоздкий)
    let user = find_user(1);
    let email = match user {
        Some(name) => find_email(&name),
        None => None,
    };
    
    // Функциональный подход с and_then (монадический bind)
    let email = find_user(1)
        .and_then(|name| find_email(&name));
    
    println!("{:?}", email); // Some("alice@example.com")
}

Продвинутый пример — использование комбинаторов map, and_then, or_else и unwrap_or_else:

fn parse_number(s: &str) -> Result<i32, String> {
    s.parse::<i32>()
        .map_err(|_| format!("Не удалось распарсить '{}'", s))
}

fn main() {
    let result = parse_number("42")
        .map(|x| x * 2)                          // Ok(84)
        .and_then(|x| {
            if x > 100 {
                Err("Слишком большое".to_string())
            } else {
                Ok(x)
            }
        })
        .or_else(|err| {
            println!("Ошибка: {}", err);
            Ok(0) // Значение по умолчанию
        });
    
    println!("{:?}", result); // Ok(84)
}

Монадический стиль позволяет избежать "пирамиды ужасов" из вложенных проверок и делает код линейным и читаемым.

4. Каррирование и композиция функций

В Rust нет встроенной поддержки каррирования, но его можно эмулировать с помощью замыканий. Каррирование — это преобразование функции от нескольких аргументов в последовательность функций от одного аргумента.

fn add(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

fn main() {
    let add5 = add(                

Блоги

Рекомендаций по книгам