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

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

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

Начать курс

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

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

1. Продвинутые замыкания: владение, заимствование и трейты

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

Захват с перемещением (move closure) используется, когда замыкание должно владеть захваченными данными, например, при передаче в новый поток:

use std::thread;

let data = vec![1, 2, 3];

let handle = thread::spawn(move || {
    println!("{:?}", data); // data перемещена в замыкание
});

handle.join().unwrap();

Замыкания как аргументы функций позволяют создавать гибкие API. Используйте обобщённые типы с трейтами-ограничениями:

fn apply_twice<F>(f: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(f(x))
}

let square = |x| x * x;
println!("{}", apply_twice(square, 2)); // Вывод: 16

Возврат замыканий — сложная задача из-за неизвестного размера типа замыкания. Используйте Box<dyn Fn> или ключевое слово impl:

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

let add_five = make_adder(5);
println!("{}", add_five(10)); // 15

2. Итераторы: ленивость, комбинаторы и производительность

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

Цепочки комбинаторов позволяют строить сложные преобразования данных без циклов:

let numbers = vec![1, 2, 3, 4, 5, 6];

let result: Vec<i32> = numbers
    .iter()
    .filter(|&&x| x % 2 == 0)   // оставляем чётные
    .map(|&x| x * 2)           // умножаем на 2
    .take(2)                   // берём первые два
    .collect();

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

Пользовательские итераторы создаются реализацией трейта Iterator. Например, итератор чисел Фибоначчи:

struct Fibonacci {
    curr: u64,
    next: u64,
}

impl Fibonacci {
    fn new() -> Self {
        Fibonacci { curr: 0, next: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let new_next = self.curr.checked_add(self.next)?;
        self.curr = self.next;
        self.next = new_next;
        Some(self.curr)
    }
}

let fib: Vec<u64> = Fibonacci::new().take(10).collect();
println!("{:?}", fib); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Производительность итераторов часто не уступает ручным циклам, а в некоторых случаях компилятор LLVM генерирует более оптимальный код.

3. Композиция функций и монадический стиль

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

Простая композиция с помощью замыканий:

fn compose<F, G, A, B, C>(f: F, g: G) -> impl Fn(A) -> C
where
    F: Fn(B) -> C,
    G: Fn(A) -> B,
{
    move |x| f(g(x))
}

let add_one = |x| x + 1;
let square = |x| x * x;
let add_one_then_square = compose(square, add_one);

println!("{}", add_one_then_square(2)); // (2+1)^2 = 9

Монадический стиль с Option и Result. Эти типы — ближайший аналог монад в Rust. Метод and_then (монадический bind) позволяет избежать "ада проверок":

fn parse_number(s: &str) -> Option<i32> {
    s.parse().ok()
}

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

// Вместо вложенных match
let result = parse_number("10")
    .and_then(|a| parse_number("2")
        .and_then(|b| safe_divide(a, b)));

println!("{:?}", result); // Some(5)

// С оператором ? в функциях, возвращающих Option/Result
fn compute(s1: &str, s2: &str) -> Option<i32> {
    let a = parse_number(s1)?;
    let b = parse_number(s2)?;
    safe_divide(a, b)
}

4. Ленивые вычисления и бесконечные структуры

Rust поддерживает ленивые вычисления через итераторы и крейт lazy_static или once_cell. Это позволяет работать с потенциально бесконечными последовательностями.

Бесконечный итератор с std::iter::repeat или пользовательскими:

// Бесконечный итератор чётных чисел
let evens = (0..).filter(|x| x % 2 == 0);

// Берём первые 5
let first_five: Vec<i32> = evens.take(5).collect();
println!("{:?}", first_five); // [0, 2, 4, 6, 8]

Блоги

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