Разные подходы для решения одной задачи#
Введение:#
В программировании приняты несколько парадигм. 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 собирает в себе лучшее из мира программирования.
Если что-то осталось непонятным, пожалуйста изучите, что делает каждая строка и прочитайте комментарии к коду. И тогда, я верю, белых пятен станет намного меньше.