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

Однако существуют случаи, когда мы можем писать декларативные операторы, которые столь же эффективны, как и их императивные аналоги, но при этом их намного легче читать и легче изменять и поддерживать. В этой статье я хотел бы выделить три декларативных сценария использования, в которых 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 мы можем рассматривать подкоманды и их аргументы как просто данные, а библиотеку обрабатывать аргументы за нас.