Функциональное программирование в Rust: от А до Я
Язык Rust, известный своей безопасной работой с памятью и высокой производительностью, также предлагает мощные инструменты для функционального программирования. В отличие от чисто функциональных языков (Haskell), Rust позволяет гибко сочетать императивный и функциональный стили. В этой статье мы разберем ключевые концепции функционального программирования в Rust: от базовых замыканий до продвинутых монадических паттернов. Материал будет полезен как новичкам, так и опытным разработчикам, желающим писать более лаконичный и надежный код.
1. Основы: Иммутабельность и переменные
Функциональное программирование начинается с иммутабельности (неизменяемости). В Rust переменные по умолчанию неизменяемы. Это ключевое отличие от многих языков.
// Иммутабельная переменная (по умолчанию)
let x = 10;
// x = 20; // Ошибка компиляции!
// Мутабельная переменная (явно)
let mut y = 5;
y = 15; // Работает
Использование let без mut гарантирует, что значение не изменится после инициализации. Это снижает количество побочных эффектов и делает код более предсказуемым. В функциональном стиле рекомендуется отдавать предпочтение неизменяемым переменным, создавая новые значения вместо изменения старых.
2. Замыкания (Closures) — анонимные функции
Замыкания — это анонимные функции, которые могут захватывать переменные из своего окружения. В Rust они реализованы через трейты Fn, FnMut и FnOnce.
2.1. Синтаксис и захват переменных
// Простое замыкание
let add_one = |x: i32| -> i32 { x + 1 };
println!("{}", add_one(5)); // 6
// Захват переменной из окружения
let base = 10;
let add_base = |x| x + base; // base захватывается по ссылке (&)
println!("{}", add_base(20)); // 30
// Захват с мутацией (FnMut)
let mut counter = 0;
let mut increment = || { counter += 1; };
increment();
increment();
println!("{}", counter); // 2
// Захват с потреблением (FnOnce)
let name = String::from("Rust");
let consume = || { drop(name); };
consume();
// println!("{}", name); // Ошибка: name перемещено
2.2. Замыкания как аргументы функций
Замыкания часто передаются в функции высшего порядка, такие как map, filter и fold.
let numbers = vec![1, 2, 3, 4, 5];
// Использование замыкания с filter
let evens: Vec<i32> = numbers
.into_iter()
.filter(|&x| x % 2 == 0)
.collect();
println!("{:?}", evens); // [2, 4]
3. Итераторы и цепочки преобразований
Итераторы — сердце функционального программирования в Rust. Они ленивы (вычисления происходят только при вызове collect() или for) и позволяют строить цепочки преобразований.
3.1. Базовые адаптеры итераторов
let data = vec![1, 2, 3, 4, 5, 6];
// Цепочка: filter -> map -> take -> collect
let result: Vec<i32> = data
.iter()
.filter(|&x| x % 2 == 0) // оставляем четные
.map(|x| x * 2) // удваиваем
.take(2) // берем первые два
.collect();
println!("{:?}", result); // [4, 8]
3.2. Свертка (fold) и редукция
fold — аналог reduce в других языках. Он аккумулирует результат, проходя по всем элементам.
let numbers = [1, 2, 3, 4, 5];
// Сумма всех чисел
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {}", sum); // 15
// Факториал
let factorial = (1..=5).fold(1, |acc, x| acc * x);
println!("5! = {}", factorial); // 120
3.3. Ленивые бесконечные итераторы
// Бесконечный итератор, берем первые 10
let first_ten: Vec<i32> = (0..)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.take(10)
.collect();
println!("{:?}", first_ten); // [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]
4. Сопоставление с образцом (Pattern Matching) и Option/Result
Сопоставление с образцом — мощный инструмент, который идеально сочетается с функциональным стилем. Вместо вложенных if-else вы используете match для обработки всех возможных вариантов.
4.1. Работа с Option и Result
Типы Option<T> и Result<T, E> — это монады в Rust. Они позволяют обрабатывать отсутствие значения или ошибки без исключений.
// Option
fn safe_divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
let result = safe_divide(10.0, 2.0);
match result {
Some(value) => println!("Result: {}", value),
None => println!("Division by zero!"),
}
// Result
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
// Использование map и and_then (монадический стиль)
let content = read_file("hello.txt")
.map(|s| s.trim().to_string())
.and_then(|s| {
if s.is_empty() {
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "empty file"))
} else {
Ok(s)
}
});
match content {
Ok(text) => println!("File content: {}", text),
Err(e) => eprintln!("Error: {}",