Перейти к содержанию

Изучение Rust - встроенные макросы

Posted on:16 июня 2023 г. at 08:03

Example Dynamic OG Image link Когда-нибудь выходили из себя, набирая тот же старый код шаблона снова и снова? Заметили закономерность и бум! Rust может взорвать код для тебя!

Как разработчик программного обеспечения, я пишу заметное количество кода. Приятно печатать и видеть поток характеристик, проявляющих мои мысли. Однако мой мыслительный процесс имеет тенденцию двигаться быстрее, чем мои пальцы на клавиатуре. Чтобы устранить это несоответствие, я иногда ищу способы минимизировать ввод текста, сохраняя скорость мышления.

В эти моменты я могу распознать возможности оптимизации, такие как извлечение повторяющегося кода в выделенную функцию. Этот шаг не только уменьшает ввод, но и упрощает обслуживание кода. Когда это делается, это часто приносит чувство удовлетворения. Однако бывают случаи, когда этот процесс может быть компромиссом: извлеченный и обобщенный код мог бы работать медленнее, чем оригинал, оставляя меня желать оптимального баланса эффективности и производительности.

В отличие от Python, Java или C#, Rust позволяет создавать код с макросами во время компиляции. Что это значит? Это означает, что можно написать код, похожий на стандартный Rust, который затем будет расширен в другой код во время компиляции. Рассмотрим хорошо известный пример использования макроса println!:

let name = "Adrian";
println!("Hello, {}", name);

Код предназначен для печати одной конкатенированной строки. В таких языках, как Java, среда выполнения будет анализировать первый аргумент, создавать местозаполнитель, а затем заменять его во время печати. Этот процесс требует дополнительных циклов ЦП и может привести к сбою. Напротив, Rust анализирует такое форматирование во время компиляции и создает следующий эквивалентный код:

let name = "Adrian";
{
    ::std::io::_print(
        ::core::fmt::Arguments::new_v1(
            &["Hello, ", "\n"],
            &[::core::fmt::ArgumentV1::new_display(&name)],
        ),
    );
};

В Rust такие макросы можно создать самостоятельно. Синтаксис трудно понять изначально, но крейт для вставки упрощает обучающую кривую. Рассмотрим пример, который включает значительный код шаблона:

struct ParserRevision {
    revision_id: Option<String>,
    revision_id_parent: Option<String>,
    sha1: Option<String>,
    timestamp: Option<String>,
    contributor_id: Option<String>,
    contributor_ip: Option<String>,
    contributor_name: Option<String>,
}

impl ParserRevision {
    fn with_revision_id(mut self, value: String) -> Self {
        self.revision_id = Some(value);
        self
    }

    fn with_revision_id_parent(mut self, value: String) -> Self {
        self.revision_id_parent = Some(value);
        self
    }

    fn with_sha1(mut self, value: String) -> Self {
        self.sha1 = Some(value);
        self
    }

    fn with_timestamp(mut self, value: String) -> Self {
        self.timestamp = Some(value);
        self
    }

    fn with_contributor_id(mut self, value: String) -> Self {
        self.contributor_id = Some(value);
        self
    }

    fn with_contributor_ip(mut self, value: String) -> Self {
        self.contributor_ip = Some(value);
        self
    }

    fn with_contributor_name(mut self, value: String) -> Self {
        self.contributor_name = Some(value);
        self
    }
}

Код демонстрирует использование изменяемого объекта построителя, заполняющего его частные поля. Для каждого поля требуется новый метод, который выглядит идентично другим. Это можно улучшить, внедрив следующий метод для выполнения:

struct ParserRevision {
    revision_id: Option<String>,
    revision_id_parent: Option<String>,
    sha1: Option<String>,
    timestamp: Option<String>,
    contributor_id: Option<String>,
    contributor_ip: Option<String>,
    contributor_name: Option<String>,
}

impl ParserRevision {
    fn with(mut self, key: &str, value: String) -> Self {
        match key {
            "revision_id" => self.revision_id = Some(value),
            "revision_id_parent" => self.revision_id_parent = Some(value),
            "sha1" => self.sha1 = Some(value),
            "timestamp" => self.timestamp = Some(value),
            "contributor_id" => self.contributor_id = Some(value),
            "contributor_ip" => self.contributor_ip = Some(value),
            "contributor_name" => self.contributor_name = Some(value),
            key => panic!("Key not found: {}", key),
        }

        self
    }
}

Эта реализация имеет несколько недостатков, таких как использование panic! и отсутствие надежных проверок действительных идентификаторов. Однако с помощью макросов Rust можно обойти эти проблемы. Мы можем создать предыдущий подробный код, который проверяет идентификаторы и не прибегает к панике:

macro_rules! with_functions {
    ($($name:ident,)*) => {
        paste::paste! {
            $(
                fn [<with_ $name>](mut self, value: String) -> Self {
                    self.$name = Some(value);
                    self
                }
            )*
        }
    };
}

impl ParserRevision {
    with_functions! {
        revision_id,
        revision_id_parent,
        sha1,
        timestamp,
        contributor_id,
        contributor_ip,
        contributor_name,
    }
}

Этот макрос берет список идентификаторов, разделенных запятыми, и генерирует соответствующий метод для каждого поля, ответственного за установку этого поля. Разве это не красивый подход?

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