В большинстве низкоуровневых языков программирования мы привыкли описывать как то, чего мы хотим, а не о том, чего мы хотим достичь в первую очередь. В некоторых случаях этот императивный подход превосходит любые другие подходы по сложности времени и памяти.
Однако существуют случаи, когда мы можем писать декларативные операторы, которые столь же эффективны, как и их императивные аналоги, но при этом их намного легче читать и легче изменять и поддерживать. В этой статье я хотел бы выделить три декларативных сценария использования, в которых Rust преуспевает.
Итераторы
В Rust реализация итераторов не составляет большого труда. Все, что вам нужно сделать для типа, - это реализовать единственную функцию fn next(&mut self) -> Option<Item>
для признака Iterator
. В свою очередь, мы автоматически получаем целый ряд методов на наших итераторах, которые позволяют нам писать простые и выразительные запросы и преобразования на наших итераторах.
Если бы мы хотели получить третью последнюю гласную ASCII из строки, реализация Rust могла бы просто перевернуть итератор по строкам, отфильтровать все, что не является гласным, и вернуть третий элемент последовательности. В качестве бонуса, если бы мы хотели добавить больше преобразований или изменить требования к функции, сделать это было бы тривиально (поскольку мы бы добавили / удалили только некоторые преобразования в цепочке). (Благодарим agersant и gatosatanico на Reddit за дальнейшее упрощение примера.)
fn third_last_vowel(input: &str) -> Option<char> { let is_vowel = |c: &char| matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'y'); input.chars().rev().filter(is_vowel).nth(2) }
Для сравнения, тривиальная реализация C ++ 17 должна быть узкоспециализированной для этого варианта использования: мы будем вручную держать счетчик, чтобы убедиться, что мы пропускаем последние две гласные, и нам пришлось бы использовать обратные итераторы (или, что еще хуже, индексы ) для итерации с обратной стороны.
std::optional<char> third_last_vowel(std::string_view input) noexcept { auto is_vowel = [](auto c) noexcept { return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y'; }; unsigned counter {0}; for (auto it = input.crbegin(); it != input.crend(); ++it) { if (!is_vowel(*it)) continue; ++counter; if (counter == 3) return *it; } return std::nullopt; }
Стандартная библиотека C ++ 17 предоставляет алгоритмы, которые могут помочь нам в решении этой задачи, хотя для этого потребуется использовать лямбда-выражения с отслеживанием состояния. Эта реализация имеет гораздо больше смысла, очевидного в коде, но то, что мы делаем, гораздо менее понятно, чем вариант на Rust, и здесь гораздо больше беспорядка.
std::optional<char> third_last_vowel(std::string_view input) noexcept { auto is_vowel = [](auto c) noexcept { return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y'; }; auto it = std::find_if( input.crbegin(), input.crend(), [&, counter {0}](auto c) mutable noexcept { if (is_vowel(c)) return ++counter == 3; return false; } ); if (it != input.crend()) return *it; return std::nullopt; }
Сериализация и десериализация
Обмениваемся ли мы с удаленной службой или загружаем настройки нашего приложения из файла, сериализация и десериализация - очень распространенная операция.
Процедурные макросы Rust позволяют библиотекам генерировать для нас много полезного кода. Библиотека serde предоставляет нам устройства для общей и эффективной генерации кода сериализации и десериализации для любой структуры Rust (включая необязательную настройку).
В этом примере все, что нам нужно для struct
или enum
, - это #[derive(Serialize, Deserialize)]
перед его объявлением, чтобы получить возможность сериализовать и десериализовать его. Затем мы используем библиотеки для JSON и TOML для сериализации и десериализации наших структур, и все это без необходимости писать ни одной строчки кода помимо объявлений derive
.
// in Cargo.toml [dependencies]: // serde = { version = "1.0", features = ["derive"] } // serde_json = "1.0" // toml = "0.5" use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] struct Settings { name: Option<String>, aggressive: bool, } #[derive(Debug, Serialize, Deserialize)] struct Data { data: Vec<String>, settings: Settings, } fn main() { // deserialize from TOML let data: Data = toml::from_str( r#" data = ["a", "b", "c"] [settings] aggressive = true "# ) .unwrap(); // serialize to JSON println!("{}", serde_json::to_string(&data).unwrap()); }
Ничего подобного в C ++ нет. Мы не можем добавить одно производное к нашим классам, чтобы сделать их сериализуемыми. Без рефлексии C ++ не сможет этого сделать в ближайшее время.
Аргументы командной строки
Другой распространенной операцией является обработка аргументов командной строки. С structopt
мы можем просто объявлять, каковы наши аргументы (включая подкоманды в нашем примере), и для нас создаются страницы обработки, синтаксического анализа и справки в командной строке.
Следующий пример достаточен, чтобы иметь программу, которая имеет
1. Две подкоманды с разными аргументами для них
2. Сообщения об ошибках для отсутствующих аргументов
3. Страницы справки для всей программы, как а также для каждой подкоманды
4. Пользовательские справочные сообщения подкоманды и значения по умолчанию для некоторых аргументов.
// in Cargo.toml [dependencies]: // structopt = "0.3" use structopt::StructOpt; #[derive(Debug, StructOpt)] struct RectangleOpts { #[structopt(short, long)] height: u8, #[structopt(short, long)] width: u8, } #[derive(Debug, StructOpt)] struct CircleOpts { #[structopt(short, long, default_value = "5")] radius: u8, } #[derive(Debug, StructOpt)] enum Args { #[structopt(about = "Draws a circle")] Circle(CircleOpts), #[structopt(about = "Draws a rectangle")] Rectangle(RectangleOpts), } fn main() { let args = Args::from_args(); println!("{:?}", args); }
Как и в предыдущем примере, в C ++ ничего подобного не существует. Хорошие фреймворки по-прежнему позволяют нам декларативно перечислять наши варианты, но ни один из них не может извлекать данные так удобно и точно, как версия Rust. В Rust мы можем рассматривать подкоманды и их аргументы как просто данные, а библиотеку обрабатывать аргументы за нас.