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