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

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

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

Начать курс

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

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

Замыкания (closures) и лямбда-выражения

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

Синтаксис замыканий лаконичен: |параметры| тело. Замыкания могут быть переданы в функции, сохранены в переменные и возвращены из функций. Важно помнить о трёх трейтах, управляющих захватом: Fn (заимствование), FnMut (изменяемое заимствование) и FnOnce (потребление).

fn main() {
    // Простое замыкание
    let add_one = |x: i32| x + 1;
    println!("{}", add_one(5)); // 6

    // Захват переменной
    let factor = 10;
    let multiply = |x| x * factor;
    println!("{}", multiply(3)); // 30

    // Замыкание с трейтом FnOnce
    let name = String::from("Rust");
    let greet = move || {
        println!("Привет, {}!", name); // name перемещено
    };
    greet();
    // println!("{}", name); // Ошибка! name больше не доступен
}

Ключевое слово move форсирует захват по значению (перемещение), что часто необходимо при работе с многопоточностью или при передаче замыкания в другой контекст.

Итераторы: ленивая обработка и цепочки методов

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

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];

    // Функциональный подход: фильтрация и преобразование
    let result: Vec<i32> = numbers
        .iter()                 // создаём итератор
        .filter(|&x| x % 2 == 0) // оставляем только чётные
        .map(|x| x * x)         // возводим в квадрат
        .collect();             // собираем в вектор

    println!("{:?}", result); // [4, 16, 36]

    // Сумма с помощью fold (редукция)
    let sum = numbers.iter().fold(0, |acc, x| acc + x);
    println!("Сумма: {}", sum); // 21

    // Проверка условия с any и all
    let has_even = numbers.iter().any(|x| x % 2 == 0);
    println!("Есть чётные: {}", has_even); // true
}

Использование итераторов делает код декларативным, читаемым и менее подверженным ошибкам, связанным с индексацией и границами массивов. Компилятор Rust часто оптимизирует цепочки итераторов до эффективного машинного кода, сравнимого с ручными циклами.

Паттерн-матчинг и деструктуризация

Паттерн-матчинг (match) — это сердце функционального управления потоком в Rust. Он позволяет безопасно обрабатывать все возможные варианты данных, особенно в сочетании с алгебраическими типами данных (enum). Паттерны могут быть вложенными, включать условия (guards) и деструктурировать структуры, кортежи и перечисления.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn handle_message(msg: Message) {
    match msg {
        Message::Quit => println!("Выход"),
        Message::Move { x, y } => {
            println!("Перемещение в ({}, {})", x, y);
        }
        Message::Write(text) => println!("Текст: {}", text),
        Message::ChangeColor(r, g, b) => {
            println!("Цвет: RGB({}, {}, {})", r, g, b);
        }
    }
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 };
    handle_message(msg);

    // Деструктуризация кортежа
    let pair = (1, "hello");
    let (a, b) = pair;
    println!("{}, {}", a, b); // 1, hello
}

Паттерн-матчинг также используется в if let и while let для удобной обработки одного конкретного варианта, что сокращает шаблонный код.

Option и Result: обработка ошибок без исключений

В функциональном программировании отсутствие значения или ошибка — это просто данные. Rust использует типы Option<T> (возможно значение) и Result<T, E> (результат или ошибка). Вместо выбрасывания исключений вы явно обрабатываете эти случаи с помощью match или комбинаторов: map, and_then, unwrap_or и других.

fn safe_divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

fn main() {
    let result = safe_divide(10.0, 2.0);
    // Комбинатор map для преобразования Option
    let doubled = result.map(|x| x * 2.0);
    println!("{:?}", doubled); // Some(10.0)

    // and_then для цепочек операций, возвращающих Option
    let chain = safe_divide(10.0, 2.0)
        .and_then(|x| safe_divide(x, 5.0));
    println!("{:?}", chain); //                

Блоги

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