Функциональное программирование в 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(