Как стать автором
Обновить
732.85
OTUS
Цифровые навыки от ведущих экспертов

Три столпа функционального программирования в Rust: map, filter и fold

Время на прочтение8 мин
Количество просмотров4.3K

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

Представьте себе вот такую картину: вы сидите дома, и вокруг вас мирно мурлыкают котики. Но вдруг, что-то пошло не по плану: один начал ловить лазерный указатель, другой карабкается на шторы, третий — нагло укладывается на вашу клавиатуру. Ну, вы поняли, полный хаос. И тут возникает вопрос: как навести порядок в этом котячьем хаосе? Как упорядочить этот бесконечный поток пушистых данных?

Вот тут-то и приходит на помощь наш добрый друг — Rust, а точнее его функции map, filter и fold. Они помогают не только приручить самых неугомонных data-котиков, но и сделать это без компромиссов по производительности.

map для трансформации потоков данных

map — это метод, применяемый к итератору, который принимает функцию в качестве аргумента и применяет её к каждому элементу коллекции, возвращая новый итератор с преобразованными элементами.

Представьте, что у вас есть список котиков, и каждый котик нуждается в небольшой трансформации, скажем, хочется переименовать их или добавить какие-то атрибуты. map позволяет сделать это.

Основная задача map — трансформация данных. Т.е берется каждая сущность из потока данных, применяется к ней функция и на выходе получается новый поток с изменёнными сущностями.

Некоторые моменты:

  • map не изменяет оригинальный итератор, а создает новый.

  • В Rust итераторы ленивы, т.е они не выполняют вычисления до тех пор, пока это не станет необходимым. Это означает, что вызов map сам по себе не выполнит никаких преобразований, пока не будет использован метод, потребляющий итератор.

Начнем с самого простого и классического примера — преобразования списка чисел:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    let squared_numbers: Vec<i32> = numbers.iter().map(|&x| x * x).collect();

    println!("{:?}", squared_numbers);
}

Здесь мы взяли вектор numbers, и с помощью map превратили каждый элемент в его квадрат. В итоге squared_numbers станет [1, 4, 9, 16, 25].

Обратите внимание на:

  • Мы использовали iter(), чтобы получить итератор по ссылкам на элементы, затем применили map, и в конце собрали результат в новый вектор с помощью collect().

  • Ленивость итераторов: map не выполняет преобразование до вызова collect.

Теперь усложним задачу. Допустим, есть список структур, и нужно извлечь из них определённое поле:

struct Cat {
    name: String,
    age: u8,
}

fn main() {
    let cats = vec![
        Cat { name: String::from("Mittens"), age: 2 },
        Cat { name: String::from("Whiskers"), age: 5 },
        Cat { name: String::from("Shadow"), age: 3 },
    ];

    let cat_names: Vec<String> = cats.iter().map(|cat| cat.name.clone()).collect();

    println!("{:?}", cat_names);
}

Здесь создали структуру Cat и список котов. С помощью map мы извлекаем имена всех котов в новый вектор. Заметьте, что пришлось использовать clone(), чтобы избежать проблем с владением (borrow checker в Rust не дремлет).

Но что если нужно преобразовать данные в соответствии с некоторым условием? Возьмем пример с преобразованием имен котов, чтобы добавить к ним возраст:

fn main() {
    let cats = vec![
        Cat { name: String::from("Mittens"), age: 2 },
        Cat { name: String::from("Whiskers"), age: 5 },
        Cat { name: String::from("Shadow"), age: 3 },
    ];

    let cat_descriptions: Vec<String> = cats.iter().map(|cat| {
        format!("{} is {} years old", cat.name, cat.age)
    }).collect();

    println!("{:?}", cat_descriptions);
}

Теперь каждый элемент стал строкой, описывающей имя кота и его возраст.

Несколько советов:

  1. Избегайте ненужных клонирований: Если нужно сохранить ссылки на оригинальные данные, лучше использовать iter() вместо into_iter(), чтобы не потреблять коллекцию.

  2. Убедитесь, что функция не имеет побочных эффектов: map предназначен для чистых преобразований. Если функция, передаваемая в map, имеет побочные эффекты (например, изменения внешнего состояния), это может привести к непредсказуемым результатам.

  3. Сочетание с другими итераторами: map отлично комбинируется с другими методами итераторов, такими как filter, enumerate и fold. Например, можно сначала отфильтровать элементы, а затем применить к ним map.

Фильтрация данных с помощью filter

По сути, filter — это условный сито, через которое просеиваются данные, и на выходе остаются только золотые крупицы.

filter принимает функцию-предикат, которая возвращает true или false. Если для элемента предикат вернул true, элемент остаётся в потоке данных; если false — он удаляется.

Предположим, есть список чисел, и нужно оставить только чётные:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let even_numbers: Vec<i32> = numbers.into_iter().filter(|&x| x % 2 == 0).collect();

    println!("{:?}", even_numbers);
}

Результат будет [2, 4, 6, 8, 10].

filter проходит по каждому элементу итератора и применяет к нему функцию-предикат. Если предикат возвращает true, элемент остаётся в выходном потоке.

Как и map, filter ленив. Это означает, что никакие данные фактически не фильтруются до тех пор, пока вы не вызовете метод, который потребляет итератор, например, collect().

Примеры использования

Допустим, есть массив чисел, и нужно выделить только те, которые делятся на 3:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let divisible_by_three: Vec<i32> = numbers.into_iter().filter(|&x| x % 3 == 0).collect();

    println!("{:?}", divisible_by_three);
}

Вывод: [3, 6, 9].

Предположим, есть список котов, и вы нужно выбрать только тех, кто старше 3 лет:

struct Cat {
    name: String,
    age: u8,
}

fn main() {
    let cats = vec![
        Cat { name: String::from("Mittens"), age: 2 },
        Cat { name: String::from("Whiskers"), age: 5 },
        Cat { name: String::from("Shadow"), age: 3 },
        Cat { name: String::from("Luna"), age: 7 },
    ];

    let adult_cats: Vec<&Cat> = cats.iter().filter(|&cat| cat.age > 3).collect();

    for cat in adult_cats {
        println!("{} is {} years old", cat.name, cat.age);
    }
}

Здесь оставляем только тех котов, возраст которых больше 3 лет. Результат:

Whiskers is 5 years old
Luna is 7 years old

Иногда нужно фильтровать данные по более сложным критериям. Например, есть список пользователей, и нужно выбрать только тех, у кого активная подписка и возраст больше 18 лет:

struct User {
    name: String,
    age: u8,
    has_active_subscription: bool,
}

fn main() {
    let users = vec![
        User { name: String::from("Alice"), age: 22, has_active_subscription: true },
        User { name: String::from("Bob"), age: 17, has_active_subscription: true },
        User { name: String::from("Charlie"), age: 19, has_active_subscription: false },
        User { name: String::from("Dave"), age: 30, has_active_subscription: true },
    ];

    let active_adults: Vec<&User> = users.iter()
        .filter(|&user| user.age > 18 && user.has_active_subscription)
        .collect();

    for user in active_adults {
        println!("{} is {} years old and has an active subscription", user.name, user.age);
    }
}

Результат:

Alice is 22 years old and has an active subscription
Dave is 30 years old and has an active subscription

Если идет работа с большими объёмами данных, стоит обратить внимание на библиотеку Rayon, которая позволяет распараллелить итераторы.

Пример:

use rayon::prelude::*;

fn main() {
    let numbers: Vec<i32> = (0..1_000_000).collect();

    let even_numbers: Vec<i32> = numbers.par_iter().filter(|&&x| x % 2 == 0).cloned().collect();

    println!("Found {} even numbers", even_numbers.len());
}

Используем параллельный итератор par_iter, чтобы ускорить фильтрацию.

Агрегирование данных с использованием fold

В Rust функция fold используется для последовательного накопления значений из потока данных. Если другими словами, это инструмент для свертки, который позволяет свести поток данных к одному значению.

Основная идея заключается в том, что fold берёт начальное значение и последовательно применяет функцию, которая принимает аккумулятор и текущий элемент потока, обновляет аккумулятор и возвращает его на следующем шаге.

Сигнатура метода fold выглядит так:

fn fold<B, F>(self, init: B, f: F) -> B
where
    F: FnMut(B, Self::Item) -> B
  • init: начальное значение аккумулятора.

  • f: функция, которая применяется к аккумулятору и каждому элементу итератора.

  • Возвращаемое значение — это аккумулятор после обработки всех элементов.

Классический пример — суммирование чисел в списке:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);

    println!("Sum: {}", sum);
}

Здесь начинаем с аккумулятора, равного 0, и на каждом шаге добавляем к нему текущее значение из потока данных. В итоге sum будет равен 15.

Допустим, есть массив строк, и нужно объединить их в одну строку:

fn main() {
    let words = vec!["Hello", "Rust", "World"];

    let sentence: String = words.iter().fold(String::new(), |mut acc, &word| {
        acc.push_str(word);
        acc.push(' ');
        acc
    });

    println!("Sentence: {}", sentence.trim());
}

Начинаем с пустой строки, и на каждом шаге добавляем к аккумулятору следующее слово. Обратите внимание на вызов trim() при выводе результата, чтобы убрать лишний пробел в конце.

Можно юзать fold для вычисления факториала числа:

fn main() {
    let n = 5;
    
    let factorial: i32 = (1..=n).fold(1, |acc, x| acc * x);

    println!("Factorial of {} is {}", n, factorial);
}

Здесь начинаем с аккумулятора, равного 1, и на каждом шаге умножаем его на текущее значение в диапазоне от 1 до n. В результате для n = 5 факториал будет равен 120.

Примеры использования

Допустим, есть список чисел и нужно получить сумму чётных чисел и сумму нечётных чисел в одной операции:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];

    let (sum_even, sum_odd) = numbers.iter().fold((0, 0), |(even_acc, odd_acc), &x| {
        if x % 2 == 0 {
            (even_acc + x, odd_acc)
        } else {
            (even_acc, odd_acc + x)
        }
    });

    println!("Sum of even numbers: {}", sum_even);
    println!("Sum of odd numbers: {}", sum_odd);
}

Здесь начинаем с кортежа (0, 0) для хранения двух аккумуляторов: для чётных и нечётных чисел. Затем на каждом шаге проверяем, является ли число чётным, и обновляем соответствующий аккумулятор.

Сложная структура данных — это не проблема для fold. Допустим, нужно сгруппировать элементы по какому-то ключу, например, по первой букве строки:

use std::collections::HashMap;

fn main() {
    let words = vec!["apple", "banana", "apricot", "blueberry", "avocado"];

    let grouped: HashMap<char, Vec<&str>> = words.iter().fold(HashMap::new(), |mut acc, &word| {
        acc.entry(word.chars().next().unwrap())
            .or_insert(Vec::new())
            .push(word);
        acc
    });

    for (key, value) in &grouped {
        println!("{}: {:?}", key, value);
    }
}

Здесь начинаем с пустого HashMap и на каждом шаге добавляем слова в соответствующие группы по первой букве.

Одной из крутых фич fold — способность работать с разными типами данных. Например, вы можете агрегировать информацию о пользователях, собирая данные из нескольких полей:

struct User {
    name: String,
    age: u8,
}

fn main() {
    let users = vec![
        User { name: String::from("Alice"), age: 30 },
        User { name: String::from("Bob"), age: 25 },
        User { name: String::from("Charlie"), age: 35 },
    ];

    let total_age: u8 = users.iter().fold(0, |acc, user| acc + user.age);
    let names: String = users.iter().fold(String::new(), |mut acc, user| {
        acc.push_str(&user.name);
        acc.push(' ');
        acc
    });

    println!("Total age: {}", total_age);
    println!("Names: {}", names.trim());
}

Здесь fold используется для суммирования возрастов пользователей и объединения их имён в одну строку.


Заключение

Можно легко объединить map, filter и fold. Например, можно сначала отфильтровать нежелательные элементы с помощью filter, затем преобразовать оставшиеся данные с помощью map, и, наконец, свести их в одно значение с помощью fold.

Пример:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let sum_of_squares_of_even_numbers: i32 = numbers.iter()
        .filter(|&&x| x % 2 == 0)  // Отфильтруем чётные числа
        .map(|&x| x * x)           // Преобразуем их в квадраты
        .fold(0, |acc, x| acc + x); // Просуммируем их

    println!("Sum of squares of even numbers: {}", sum_of_squares_of_even_numbers);
}

Так что не стесняйтесь комбинировать map, filter, fold и другие методы итераторов в своих проектах!

Теперь настало время внедрить их в свои проекты и ощутить всю силу функционального программирования на Rust.

Greenplum, аналитическая MPP СУБД, внезапно перестала быть доступной системой с открытым исходным кодом. Как это влияет на индустрию? Какие системы её могут заменить? Придётся ли менять архитектуру систем обработки данных? Обсудим это на открытом уроке 21 августа.

На уроке будет произведен анализ текущей ситуации, а также рассмотрены варианты дальнейших действий. В результате урока слушатели получат рекомендации по построению систем обработки данных на альтернативных решениях, которые далее смогут использовать в работе. Записывайтесь по ссылке.

Теги:
Хабы:
Всего голосов 17: ↑11 и ↓6+14
Комментарии15

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS