Разные подходы для решения одной задачи#
Введение:#
В программировании приняты несколько парадигм. 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
ok#
Метод ok преобразует Result в Option. Если результат Ok, то возвращается Some, в противном случае возвращается None.
parse::<F>()#
Механизм parse::
let four: u32 = "4".parse().unwrap();
assert_eq!(4, four);
Заключение:#
Надеюсь, что заметка поможет еще лучше познакомиться с языком Rust и увидеть, что это современный гибкий язык не заточенный на какой-то один стиль написания кода.
Rust собирает в себе лучшее из мира программирования.
Если что-то осталось непонятным, пожалуйста изучите, что делает каждая строка и прочитайте комментарии к коду. И тогда, я верю, белых пятен станет намного меньше.