Привет, Хабр!
Функциональное программирование предлагает такие концепции, как иммутабельность данных, чистые функции (т.е, результат работы которых зависит только от входных данных и не производят побочных эффектов), а также функции высшего порядка, которые позволяют работать с функциями так же, как с данными. Эти идеи вносят определенную строгость и предсказуемость.
Хоть Rust не является чистым функциональным языком программирования, однако он имеет множество инструментов, позволяющих применять функциональные принципы.
Rust поддерживает рекурсию, хотя и без оптимизации хвостовых вызовов, что является отступлением от некоторых традиционных функциональных языков, таких как Haskell. Тем не менее, язык предоставляет мощные абстракции и паттерны, такие как владение и заимствование.
Rust также поощряет иммутабельность данных по умолчанию, что является в целом базой ФП. Переменные в Rust иммутабельны по умолчанию, и язык требует явного указания mut
, если хочется изменить значение.
Кроме того, Rust имеет поддержку функций высшего порядка и замыканий.
Основы функциональности в Rust
В Rust переменные неизменяемы по умолчанию:
fn main() {
let x = 5;
println!("The value of x is: {x}");
// x = 6; // вызовет ошибку компиляции, так как x неизменяема
}
Для создания изменяемой переменной используется слово mut
:
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is now: {x}");
}
enum
в Rust позволяет определить тип, который может принимать одно из нескольких возможных значений. Совместно с match
выражениями, выходит хороший инструмент для управления потоком программы на основе различных значений enum
:
enum Direction {
Up,
Down,
Left,
Right,
}
fn move_direction(direction: Direction) {
match direction {
Direction::Up => println!("Moving up"),
Direction::Down => println!("Moving down"),
Direction::Left => println!("Moving left"),
Direction::Right => println!("Moving right"),
}
}
Функции высших порядков принимают одну или несколько функций в качестве параметров или возвращают другую функцию как результат. Замыкания в Rust - это анонимные функции, которые могут захватывать переменные из окружающей среды.
Fn
позволяет заимствовать данные из замыкающей среды неизменяемым образом.FnMut
применяется, когда замыкание должно изменять данные, заимствуя их изменяемым образом.FnOnce
используется, когда замыкание захватывает данные из окружения, перемещая их в себя. Такое замыкание может быть вызвано только один раз!
К примеру нужно выполнить операцию над числом, используя функцию, передаваемую в качестве аргумента:
fn apply<F>(value: i32, f: F) -> i32
where
F: Fn(i32) -> i32,
{
f(value)
}
fn main() {
let double = |x| x * 2;
let result = apply(5, double);
println!("Result: {}", result); // выведет: Result: 10
}
apply
принимает значение и замыкание, удваивающее это значение, а затем применяет это замыкание к значению.
Если нужно изменить значение, которое захватили в замыкании, юзаемFnMut
:
fn apply_mut<F>(mut value: i32, mut f: F) -> i32
where
F: FnMut(i32) -> i32,
{
f(value)
}
fn main() {
let mut accumulator = 1;
let multiply = |x| {
accumulator *= x;
accumulator
};
let result = apply_mut(5, multiply);
println!("Result: {}", result); // выведет: Result: 5
}
Замыкание изменяет захваченную переменную accumulator
, умножая ее на переданное значение.
FnOnce
используется, когда замыкание захватывает переменную по значению, что означает, что оно может быть вызвано только один раз.
fn apply_once<F>(value: i32, f: F) -> i32
where
F: FnOnce(i32) -> i32,
{
f(value)
}
fn main() {
let add = |x| x + 5;
let result = apply_once(5, add);
println!("Result: {}", result); // выведет: Result: 10
}
В этом случае замыкание просто добавляет 5 к переданному значению, но теоретически оно могло бы захватывать и перемещать значения из своего окружения.
Монады
Option<T>
используется, когда значение может отсутствовать. Он предоставляет два варианта: Some(T)
, когда значение присутствует, и None
, когда значения нет. Это позволяет явно обрабатывать случаи отсутствия значения, избегая ошибок, связанных с null
:
fn find_index(needle: &str, haystack: &[&str]) -> Option<usize> {
haystack.iter().position(|&s| s == needle)
}
Функция ищет строку в массиве и возвращает индекс найденной строки как Some(usize)
или None
, если строка не найдена.
Для работы с Option<T>
можно использовать различные методы, такие как match
, if let
, unwrap
, expect
и многие другие...
Result<T, E>
используется для обработки операций, которые могут завершиться ошибкой. Он предоставляет два варианта: Ok(T)
, когда операция успешно выполнена, и Err(E)
, когда произошла ошибка:
fn divide(numerator: f64, denominator: f64) -> Result<f64, &'static str> {
if denominator == 0.0 {
Err("Division by zero")
} else {
Ok(numerator / denominator)
}
}
Функция выполняет деление и возвращает результат как Ok(f64)
или ошибку Err(&str)
, если знаменатель равен нулю.
Для работы с Result<T, E>
также доступно множество методов и паттернов, включая match
, unwrap
, expect
, ?
оператор и другие.
Как Option<T>
, так и Result<T, E>
поддерживают монадические операции, такие как map
и and_then
, которые позволяют преобразовывать значения внутри этих типов, не извлекая их:
let maybe_number: Option<i32> = Some(5);
let maybe_number_plus_one = maybe_number.map(|n| n + 1); // Результат: Some(6)
Пример использования and_then
с Result<T, E>
для последовательного выполнения операций, каждая из которых может вернуть ошибку:
fn try_parse_and_divide(text: &str, divider: f64) -> Result<f64, &'static str> {
let parsed: f64 = text.parse().map_err(|_| "Parse error")?;
divide(parsed, divider)
}
map, fold, и filter
map
применяет функцию к каждому элементу итерируемого объекта, создавая новую коллекцию с результатами. В Rust map
— это метод итераторов, который принимает замыкание, применяемое к каждому элементу:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let squares: Vec<_> = nums.iter().map(|&x| x * x).collect();
println!("Squares: {:?}", squares);
// выведет: Squares: [1, 4, 9, 16, 25]
}
Здесь мы юзаем map
для создания нового вектора, содержащего квадраты чисел исходного вектора.
Fold
принимает начальное значение и замыкание, которое "сворачивает" или "редуцирует" элементы коллекции в одно значение, применяя замыкание последовательно к каждому элементу и аккумулируя результат:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let sum: i32 = nums.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {}", sum);
// выведет: Sum: 15
}
Используем fold
для подсчёта суммы чисел в векторе, начиная с 0.
Filter
создаёт итератор, который выбирает элементы коллекции, соответствующие заданному условию:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let even_nums: Vec<_> = nums.into_iter().filter(|x| x % 2 == 0).collect();
println!("Even numbers: {:?}", even_nums);
// выведет: Even numbers: [2, 4]
}
Их конечно же можно комбинировать для создания сложных цепочек:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let result: i32 = nums
.into_iter()
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.fold(0, |acc, x| acc + x);
println!("Sum of squares of even numbers: {}", result);
// выведет: Sum of squares of even numbers: 20
}
Сочетаем filter
, map
, и fold
для подсчёта суммы квадратов только чётных чисел из вектора.
И хотя Rust не был создан с мыслью о функциональном программировании в качестве своей основы, он удивительно гибок в принятии этой парадигмы.
Больше про языки программирования эксперты OTUS рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.