Перейти к основному содержимому
  1. Rust/

Rust императивный и функциональный, а также о методе collect

1030 слов·5 минут· loading · loading · ·
Rust Dev
about-rust - Эта статья часть цикла.
Часть 12: Эта статья

Разные подходы для решения одной задачи
#

Введение:
#

В программировании приняты несколько парадигм. Cегодня остановлюсь и рассмотрю императивный и функциональные стили.

Давайте напишем код который выполняет одинаковую задачу, но использует при этом различные подходы. Но до этого краткая справка по стилям.

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

Функциональное программирование плотно интегрировано в современные языки: будь то js, ruby, java, dart и др. Map, reduce, filter наверняка уже где-то встречались 😉

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

Если хотите углубиться в функциональную тему, то вот замечательная книга: “Грокаем функциональное мышление” Эрика Норманда

Приложение:
#

Давайте представим что нам нужно написать приложение для умного дома которое фильтрует и обрабатывает данные от сенсоров.

Определим структуру для данных от датчиков
#

Пусть это будет температурный датчик DS18B20 с тремя ногами и работющий по протоколу 1-wire. Это не так важно для нашего примера, но зато позволяет проникнуть в мир умного дома и немного оживляет повествование 😁

#[derive(Debug)]
struct Sensor {
    id: String,
    temperature: f32,
}

Определили структуру для сенсора, переходим к основному коду.

Примечание: derive(Debug) trait помогает при отладке кода и позволяет вывести информацию об объекте передав {:?} или {:#?} при форматировании строки. Пример -> println!("New sensor found: {:?}", Sensor { id: "123".to_string(), temperature: 20.0 }); подробнее в доке

Императивный стиль
#

В основной функции обрабатываем список показаний с датчиков, чтобы отсеять те, которые не имеют критический уровень температуры.

fn main() {
    let sensors = vec![
        "sensor1 22.5",
        "sensor2 35.7",
        "sensor3 29.8",
        "sensor4 41.3",
        "sensor5 30.0"
    ];

    // Критическое значение для температуры
    let critical_threshold = 30.0;

    // Массив датчиков со значением превышающим critical_threshold
    let mut critical_sensors = vec![];


    // проходимся циклом по датчикам
   for s in sensors {

        // разделяем id датчика и значение температуры по пробелу
        let mut s = s.split(' ');

        // получаем id датчика
        let id = s.next();

        // получаем температуру датчика
        let temperature = s.next();

        // Проверка на Some в Option
        if id.is_some() && temperature.is_some() {

            // получаем строковое значение id
            let id = id.unwrap().to_owned();

            // получаем f32 число для температуры
            let temperature = temperature.unwrap().parse::<f32>();

            // проверка на ошибку
            if temperature.is_ok() {

                // извлекаем значение из Result
                let temperature = temperature.unwrap();

                // проверка, что значение выше порога
                if temperature > critical_threshold {

                    // если все прошло успешно добавляем значение в массив сенсоров
                    critical_sensors.push(Sensor { id, temperature });
                }
            }
        }
    }

    // проходимся по всем датчикам в массиве critical_sensors
    for s in critical_sensors {

        // выводим в консоль найденные датчики
        println!("{:?}", s);
    }
}

Запускаем и видим, что ожидалось (если не видите вывода в godbolt - нажмите внизу на output):

Execution build compiler returned: 0
Program returned: 0
Sensor { id: "sensor2", temperature: 35.7 }
Sensor { id: "sensor4", temperature: 41.3 }

Получился работающий код, представляющий императивный подход. Какие особенности можно отметить:

  • большое кол-во строчек кода,
  • много инициализаций переменных,
  • аллоцирование памяти под коллекцию critical_sensors.

Переходим к функциональному подходу.

Функциональный стиль
#

Код до массива critical_sensors будет аналогичным, поэтому его пропущу

...

fn main() {
    ...
    // Массив датчиков со значением превышающим critical_threshold
    // Получаем итератор из Vec sensors
    let critical_sensors: Vec<Sensor> = sensors.iter()
        // Передаем по цепочке в функцию map
        .map(|s| {
            // разделяем id датчика и значение температуры по пробелу
            let mut s = s.split(' ');
          
            // получаем строковое значение id
            let id = s.next()?.to_owned();

            // получаем f32 число для температуры
            let temperature = s.next()?.parse::<f32>().ok()?;

            // Возвращаем Some с данными по сенсору
            Some(Sensor { id, temperature })
        })

        // Итератор который сплющивает Some(Some(Sensor)) в Iterator(Some)
        .flatten()

        // Отфильтровываем значение температуры чтобы получить те сенсоры где значение выше порога
        .filter(|s| s.temperature > critical_threshold)
        
        // Магический метод collect позволяет собрать все значения из итератора в коллекцию Vec
        .collect();

    // проходимся по всем датчикам в массиве critical_sensors
    for s in critical_sensors {

        // выводим в консоль найденные датчики
        println!("{:?}", s);
    }

}

Такой код намного проще читать, запускаем и видим вывод аналогичный прежнему:

Program returned: 0
Sensor { id: "sensor2", temperature: 35.7 }
Sensor { id: "sensor4", temperature: 41.3 }

Iterator::collect
#

Плюс использования Iterator::collect что нам не нужно аллоцировать лишнюю коллекцию и добавлять туда элементы в цикле. Метод collect позволяет взять итератор и конвертировать его в коллекцию для удобной дальнейшей работы или вывода.

Еще один небольшой пример:

    let input = vec![
        "Hello".to_string(),
        "World!".to_string(),
        "From".to_string(),
        "E@Gle".to_string(),
        "Blog".to_string(),
    ];
    
    let output: Vec<String> = input.iter().cloned().collect();
    println!("{:?}", output);

Бонус
#

Предлагаю пройтись по методам которые могут вызвать вопросы, а ведь они часто встречаются в исходниках программ написанных на rust

is_some
#

Метод is_some вызывается для типа Option и возвращает true, если Option является Some, и false, если None.

to_owned
#

Метод to_owned берет копию объекта и передаёт владение вызывающей стороне или как написано в доке преобразует заимствованные данные в собственные. Обычно он используется для создания String из &str.

unwrap
#

Метод unwrap извлекает значение из Option, если оно является Some, иначе выбрасывает панику, если Option является None.

Считается, что лучше использовать его пореже поскольку с паникой завершается выполнение программы. Про обработку ошибок будет в другой статье.

flatten
#

Метод flatten преобразует Option<Option> в Option, удаляя один уровень вложенности. Его также можно использовать с итераторами для удаления значений None.

ok
#

Метод ok преобразует Result в Option. Если результат Ok, то возвращается Some, в противном случае возвращается None.

parse::<F>()
#

Механизм parse::() в Rust используется для преобразования фрагмента строки (&str) в число. Этот метод реализован в &str и может парсить строковый слайс в различные типы, не только f32. Ниже пример показывающий как можно преобразовать &str в u32

let four: u32 = "4".parse().unwrap();

assert_eq!(4, four);

Заключение:
#

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

Rust собирает в себе лучшее из мира программирования.

Если что-то осталось непонятным, пожалуйста изучите, что делает каждая строка и прочитайте комментарии к коду. И тогда, я верю, белых пятен станет намного меньше.

Хочу напомнить: не обязательно скролить блог чтобы читать статьи можно также нажать “пробел”.
Пишите код, практикуйтесь, делитесь ссылкой на блог и удачи! 👾

about-rust - Эта статья часть цикла.
Часть 12: Эта статья

Связанные статьи

Pattern matсhing
606 слов·3 минут· loading · loading
Rust Dev
Basic rust
326 слов·2 минут· loading · loading
Rust Dev
Ссылки на полезные ресурсы по языку rust
60 слов·1 минута· loading · loading
Rust Dev