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

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

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

Начать курс

Введение в функциональное программирование на Rust

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

Основы функционального подхода: неизменяемость и чистые функции

Функциональное программирование строится на двух столпах: неизменяемость данных и чистые функции. Rust поощряет неизменяемость — по умолчанию все переменные иммутабельны. Чтобы изменить значение, нужно явно указать mut.

fn main() {
    let x = 5; // неизменяемая переменная
    // x = 6; // ошибка компиляции
    let mut y = 10;
    y += 5; // разрешено, так как y — mutable
    println!("x = {}, y = {}", x, y);
}

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

fn add(a: i32, b: i32) -> i32 {
    a + b // чистая функция
}

Замыкания (closures) — анонимные функции

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

Простой пример замыкания

fn main() {
    let factor = 2;
    let multiply = |x: i32| x * factor;
    println!("{}", multiply(10)); // 20
}

Замыкание с изменяемым захватом

fn main() {
    let mut counter = 0;
    let mut increment = || {
        counter += 1;
        counter
    };
    println!("{}", increment()); // 1
    println!("{}", increment()); // 2
}

Замыкания широко используются с итераторами и для передачи поведения в функции высшего порядка.

Итераторы и функциональные цепочки

Итераторы в Rust — одна из самых мощных функциональных абстракций. Вместо циклов вы можете строить цепочки из методов map, filter, fold и других. Итераторы ленивы — вычисления происходят только при вызове потребителя (например, 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) — аккумуляция значений

fn main() {
    let numbers = vec![1, 2, 3, 4];
    let sum = numbers.iter().fold(0, |acc, x| acc + x);
    println!("Сумма: {}", sum); // 10
}

Итераторы делают код декларативным: вы описываете что нужно сделать, а не как. Это снижает вероятность ошибок и улучшает читаемость.

Сопоставление с образцом (pattern matching)

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

Базовый пример

fn describe_number(n: i32) -> &'static str {
    match n {
        0 => "ноль",
        1 | 2 => "маленькое число",
        3..=10 => "среднее число",
        _ if n < 0 => "отрицательное",
        _ => "большое число",
    }
}

fn main() {
    println!("{}", describe_number(7));  // среднее число
    println!("{}", describe_number(-5)); // отрицательное
}

Обработка Option и Result

fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None
    } else {
        Some(a / b)
    }
}

fn main() {
    match divide(10.0, 2.0) {
        Some(result) => println!("Результат: {}", result),
        None => println!("Деление на ноль!"),
    }
}

Сопоставление с образцом — это не просто замена if-else, а мощный инструмент для работы с алгебраическими типами данных.

Функторы и монады: Option и Result

В функциональных языках функтор — это тип, для которого определён map, а монада — тип с flat_map (или and_then в Rust). Option и Result реализуют обе концепции, что позволяет строить цепочки без вложенных проверок.

Цепочка с Option

fn parse_and_double(input: &str) -> Option<i32> {
    input
        .parse::<i32>() // Option<i32>
        .ok()            // преобразуем Result в Option
        .map(|x| x * 2)  // функтор: применяем функцию
}

fn main() {
    println!("{:?}", parse_and_double("10")); // Some(20)
    println!("{:?}", parse_and_double("abc")); // None
}

Комбинация с and_then (монадический bind)

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

fn main()                

Блоги

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