
Привет, Хабр!
Функциональное программирование предлагает такие концепции, как иммутабельность данных, чистые функции (т.е, результат работы которых зависит только от входных данных и не производят побочных эффектов), а также функции высшего порядка, которые позволяют работать с функциями так же, как с данными. Эти идеи вносят определенную строгость и предсказуемость.
Хоть 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 рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/802995/
Добавить комментарий