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

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

Хоть 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 рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.

Комментарии (5)


  1. kozlov_de
    02.04.2024 05:39

    Пусть сначала rust немного подrustет

    Как появится поддержка нормального ide:

    IntelliJ IDEA, eclipse, visual studio (не Code)

    Так моя производительность в нём выrustет


    1. bambruysk
      02.04.2024 05:39
      +3

      Jetbrains RustRover

      Но возможности языка и IDE это разные вещи


    1. Dominux
      02.04.2024 05:39

      rust_analyzer - лучший LSP за всю историю человечества


  1. hrls
    02.04.2024 05:39
    +4

    Состояние функционального программирования в rust и полнота рассмотрения темы в этой статье находятся примерно на одном уровне. К сожалению, все еще далеко от уровня «приемлемо».


  1. qalisander
    02.04.2024 05:39

    Кстати можете добавить, что Option<T> и Result<T, E> по сути являются итераторами, которые при итерации возвращают один элемент если что-то есть внутри или ничего в противном случае