В общих чертах про функциональное программирование в Rust

от автора

Привет, Хабр!

Функциональное программирование предлагает такие концепции, как иммутабельность данных, чистые функции (т.е, результат работы которых зависит только от входных данных и не производят побочных эффектов), а также функции высшего порядка, которые позволяют работать с функциями так же, как с данными. Эти идеи вносят определенную строгость и предсказуемость.

Хоть 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *