Главная страница
Навигация по странице:

  • Разница между макросами и функциями

  • Декларативные макросы с

  • Листинг 19-28: Упрощённая версия определения макроса

  • Процедурные макросы для генерации кода из атрибутов

  • Листинг 19-29: Пример использования процедурного макроса

  • Как написать пользовательский

  • Листинг 19-30: Код, который сможет писать пользователь нашего крейта при использовании нашего процедурного макроса

  • Листинг 19-31: Код, который потребуется в большинстве процедурных макро крейтов для обработки Rust кода

  • Язык программирования Rust


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница55 из 62
    1   ...   51   52   53   54   55   56   57   58   ...   62

    help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
    |
    1 | fn returns_closure() -> impl Fn(i32) -> i32 {
    |


    For more information about this error, try `rustc --explain E0746`. error: could not compile `functions-example` due to previous error fn returns_closure
    () ->
    Box
    <
    dyn
    Fn
    (
    i32
    ) -> i32
    > {
    Box
    ::new(|x| x +
    1
    )
    }

    Этот код просто отлично компилируется. Для получения дополнительной информации об типаж-объектах обратитесь к разделу "Использование типаж-объектов которые допускают значения разных типов"
    главы 17.
    Далее давайте посмотрим на макросы!

    Макросы
    Мы использовали макросы, такие как println!
    на протяжении всей этой книги, но мы не изучили полностью, что такое макрос и как он работает. Термин макрос относится к семейству возможностей в Rust. Это декларативные (declarative) макросы с помощью macro_rules!
    и три вида процедурных (procedural) макросов:
    Пользовательские (выводимые)
    #[derive]
    макросы, которые указывают код добавленный с помощью derive атрибута, используемые для структур и перечислений
    Макросы подобные атрибутам (attribute-like), которые определяют настраиваемые атрибуты, используемые для любого элемента языка
    Функционально подобные (function-like) макросы, которые выглядят как вызовы функций, но работают с TokenStream
    Мы поговорим о каждом из них по очереди, но сначала давайте рассмотрим, зачем вообще нужны макросы, если есть функции.
    Разница между макросами и функциями
    По сути, макросы являются способом написания кода, который записывает другой код,
    что известно как мета программирование. В приложении C мы обсуждаем атрибут derive
    , который генерирует за вас реализацию различных типажей. Вы также использовали макросы println!
    и vec!
    в книге. Все эти макросы раскрываются для генерации большего количества кода, чем исходный код написанный вами вручную.
    Мета программирование полезно для уменьшения объёма кода, который вы должны написать и поддерживать, что также является одним из предназначений функций.
    Однако макросы имеют некоторые дополнительные возможности, которых функции не имеют.
    Сигнатура функции должна объявлять некоторое количество и тип этих параметров имеющихся у функции. Макросы, с другой стороны, могут принимать переменное число параметров: мы можем вызвать println!("hello")
    с одним аргументом или println!
    ("hello {}", name)
    с двумя аргументами. Также макросы раскрываются до того как компилятор интерпретирует смысл кода, поэтому макрос может, например, реализовать типаж заданного типа. Функция этого не может, потому что она вызывается во время выполнения и типаж должен быть реализован во время компиляции.
    Обратной стороной реализации макроса вместо функции является то, что определения макросов являются более сложными, чем определения функций, потому что вы создаёте
    Rust код, который записывает другой Rust код. Из-за этой косвенности, объявления макросов, как правило, труднее читать, понимать и поддерживать, чем объявления функций.

    Другое важное различие между макросами и функциями заключается в том, что вы должны объявить макросы или добавить их в область видимости прежде чем можете вызывать их в файле, в отличии от функций, которые вы можете объявить где угодно и вызывать из любого места.
    Декларативные макросы с macro_rules! для общего мета
    программирования
    Наиболее широко используемой формой макросов в Rust являются декларативные
    макросы. Они также иногда упоминаются как "макросы на примере", "
    macro_rules!
    макрос" или просто "макросы". По своей сути декларативные макросы позволяют писать нечто похожее на выражение match в Rust. Как обсуждалось в главе 6, match выражения являются управляющими структурами, которые принимают некоторое выражение,
    результат значения выражения сопоставляют с шаблонами, а затем запускают код для сопоставляемой ветки. Макросы также сравнивают значение с шаблонами, которые связаны с конкретным кодом: в этой ситуации значение является литералом исходного кода Rust, переданным в макрос. Шаблоны сравниваются со структурами этого исходного кода и при совпадении код, связанный с каждым шаблоном, заменяет код переданный макросу. Все это происходит во время компиляции.
    Для определения макроса используется конструкция macro_rules!
    . Давайте рассмотрим,
    как использовать macro_rules!
    глядя на то, как объявлен макрос vec!
    . В главе 8
    рассказано, как можно использовать макрос vec!
    для создания нового вектора с определёнными значениями. Например, следующий макрос создаёт новый вектор,
    содержащий три целых числа:
    Мы также могли использовать макрос vec!
    для создания вектора из двух целых чисел или вектора из пяти строковых срезов. Мы не смогли бы использовать функцию, чтобы сделать то же самое, потому что мы не знали бы заранее количество или тип значений.
    В листинге 19-28 приведено несколько упрощённое определение макроса vec!
    Файл: src/lib.rs let v:
    Vec
    <
    u32
    > = vec!
    [
    1
    ,
    2
    ,
    3
    ];

    Листинг 19-28: Упрощённая версия определения макроса
    vec!
    Примечание: фактическое определение макроса vec!
    в стандартной библиотеке включает сначала код для предварительного выделения правильного объёма памяти. Этот код является оптимизацией, которую мы здесь не включаем, чтобы сделать пример проще.
    Аннотация
    #[macro_export]
    указывает, что данный макрос должен быть доступен всякий раз, когда крейт с объявленным макросом, добавлен в область видимости. Без этой аннотации макрос нельзя добавить в область видимости.
    Затем мы начинаем объявление макроса с помощью macro_rules!
    и имени макроса,
    который объявляется без восклицательного знака. Название, в данном случае vec
    , после которого следуют фигурные скобки, указывающие тело определения макроса.
    Структура в теле макроса vec!
    похожа на структуру match выражения. Здесь у нас есть одна ветвь с шаблоном
    ( $( $x:expr ),* )
    , затем следует ветвь
    =>
    и блок кода,
    связанный с этим шаблоном. Если шаблон сопоставлен успешно, то соответствующий блок кода будет сгенерирован. Учитывая, что данный код является единственным шаблоном в этом макросе, существует только один действительный способ сопоставления, любой другой шаблон приведёт к ошибке. Более сложные макросы будут иметь более чем одна ветвь.
    Допустимый синтаксис шаблона в определениях макросов отличается от синтаксиса шаблона рассмотренного в главе 18, потому что шаблоны макроса сопоставляются со структурами кода Rust, а не со значениями. Давайте пройдёмся по тому, какие части шаблона в листинге 19-28 что означают; полный синтаксис макроса см. в ссылке
    Во-первых, набор скобок охватывает весь шаблон. Далее идёт знак доллара (
    $
    ), затем следует набор скобок, который захватывает значения, соответствующие шаблону в скобках для использования в коде замены. Внутри
    $()
    находится
    $x:expr
    , который соответствует любому выражению Rust и даёт выражению имя
    $x
    #[macro_export]
    macro_rules!
    vec {
    ( $( $x:expr ),* ) => {
    { let mut temp_vec =
    Vec
    ::new();
    $( temp_vec.push($x);
    )* temp_vec
    }
    };
    }

    Запятая, следующая за
    $()
    указывает на то, что буквенный символ-разделитель запятой может дополнительно появиться после кода, который соответствует коду в
    $()
    Звёздочка
    *
    указывает, что шаблон соответствует ноль или больше раз тому, что предшествует
    *
    Когда вызывается этот макрос с помощью vec![1, 2, 3];
    шаблон
    $x соответствует три раза всем трём выражениям
    1
    ,
    2
    и
    3
    Теперь давайте посмотрим на шаблон в теле кода, связанного с этой ветвью: temp_vec.push()
    внутри
    $()*
    генерируется для каждой части, которая соответствует символу
    $()
    в шаблоне ноль или более раз в зависимости от того, сколько раз шаблон сопоставлен. Символ
    $x заменяется на каждое совпадающее выражение. Когда мы вызываем этот макрос с vec![1, 2, 3];
    , сгенерированный код, заменяющий этот вызов макроса будет следующим:
    Мы определили макрос, который может принимать любое количество аргументов любого типа и может генерировать код для создания вектора, содержащего указанные элементы.
    Есть несколько странных краевых случаев у макроса macro_rules!
    . В будущем у Rust будет второй вид декларативного макроса, который будет работать аналогичным образом, но поправит некоторые из этих краевых случаев. После этого обновления macro_rules!
    будет фактически устаревшим. Имея это в виду, а также тот факт, что большинство Rust программистов будут использовать макросы больше, чем сами писать
    макросы, мы далее не будем обсуждать macro_rules!
    . Чтобы узнать больше о том, как писать макросы, обратитесь к электронной документации или другим ресурсам, таким как
    “The Little Book of Rust Macros”
    Процедурные макросы для генерации кода из атрибутов
    Вторая форма макросов - это процедурные макросы (procedural macros), которые действуют как функции (и являются типом процедуры). Процедурные макросы принимают некоторый код в качестве входных данных, работают над этим кодом и создают некоторый код в качестве вывода, а не выполняют сопоставления с шаблонами и замену кода другим кодом, как это делают декларативные макросы.
    Все три вида процедурных макросов (пользовательские выводимые, похожие на атрибуты и похожие на функции) все работают аналогично.
    { let mut temp_vec =
    Vec
    ::new(); temp_vec.push(
    1
    ); temp_vec.push(
    2
    ); temp_vec.push(
    3
    ); temp_vec
    }

    При создании процедурных макросов объявления должны находиться в собственном крейте специального типа. Это из-за сложных технических причин, которые мы надеемся будут устранены в будущем. Использование процедурных макросов выглядит как код в листинге 19-29, где some_attribute является заполнителем для использования специального макроса.
    Файл: src/lib.rs
    Листинг 19-29: Пример использования процедурного макроса
    Функция, которая определяет процедурный макрос, принимает
    TokenStream в качестве входных данных и создаёт
    TokenStream в качестве вывода. Тип
    TokenStream объявлен крейтом proc_macro
    , включённым в Rust и представляет собой последовательность токенов. Это ядро макроса: исходный код над которым работает макрос, является входным
    TokenStream
    , а код создаваемый макросом является выходным
    TokenStream
    . К
    функции имеет также прикреплённый атрибут, определяющий какой тип процедурного макроса мы создаём. Можно иметь несколько видов процедурных макросов в одном и том же крейте.
    Давайте посмотрим на различные виды процедурных макросов. Начнём с пользовательского, выводимого (derive) макроса и затем объясним небольшие различия,
    делающие другие формы отличающимися.
    Как написать пользовательский derive макрос
    Давайте создадим крейт с именем hello_macro
    , который определяет типаж с именем
    HelloMacro и имеет одну с ним ассоциированную функцию с именем hello_macro
    Вместо того, чтобы пользователи нашего крейта самостоятельно реализовывали типаж
    HelloMacro для каждого из своих типов, мы предоставим им процедурный макрос, чтобы они могли аннотировать свой тип с помощью атрибута
    #[diverve(HelloMacro)]
    и получили реализацию по умолчанию для функции hello_macro
    . Реализация по умолчанию выведет
    Hello, Macro! My name is TypeName!
    , где
    TypeName
    - это имя типа,
    для которого был определён этот типаж. Другими словами, мы напишем крейт,
    использование которого позволит другому программисту писать код показанный в листинге 19-30.
    Файл: src/main.rs use proc_macro;
    #[some_attribute]
    pub fn some_name
    (input: TokenStream) -> TokenStream {
    }

    Листинг 19-30: Код, который сможет писать пользователь нашего крейта при использовании нашего
    процедурного макроса
    Этот код напечатает
    Hello, Macro! My name is Pancakes!
    , когда мы закончим. Первый шаг - создать новый, библиотечный крейт так:
    Далее, мы определим типаж
    HelloMacro и ассоциированную с ним функцию:
    Файл: src/lib.rs
    У нас есть типаж и его функция. На этом этапе пользователь крейта может реализовать типаж для достижения желаемой функциональности, так:
    Тем не менее, ему придётся написать блок реализации для каждого типа, который он хотел использовать вместе с hello_macro
    ; а мы хотим избавить их от необходимости делать эту работу.
    Кроме того, мы пока не можем предоставить функцию hello_macro с реализацией по умолчанию, которая будет печатать имя типа, для которого реализован типаж: Rust не имеет возможностей рефлексии (reflection), поэтому он не может выполнить поиск имени use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro;
    #[derive(HelloMacro)]
    struct
    Pancakes
    ; fn main
    () {
    Pancakes::hello_macro();
    }
    $
    cargo new hello_macro --lib pub trait
    HelloMacro
    { fn hello_macro
    ();
    } use hello_macro::HelloMacro; struct
    Pancakes
    ; impl
    HelloMacro for
    Pancakes { fn hello_macro
    () { println!
    (
    "Hello, Macro! My name is Pancakes!"
    );
    }
    } fn main
    () {
    Pancakes::hello_macro();
    }
    типа во время выполнения кода. Нам нужен макрос для генерации кода во время компиляции.
    Следующим шагом является определение процедурного макроса. На момент написания этой статьи процедурные макросы должны быть в собственном крейте. Со временем это ограничение может быть отменено. Соглашение о структурировании крейтов и макросов является следующим: для крейта с именем foo
    , его пользовательский, крейт с выводимым процедурным макросом называется foo_derive
    . Давайте начнём с создания нового крейта с именем hello_macro_derive внутри проекта hello_macro
    :
    Наши два крейта тесно связаны, поэтому мы создаём процедурный макрос-крейт в каталоге крейта hello_macro
    . Если мы изменим определение типажа в hello_macro
    , то нам придётся также изменить реализацию процедурного макроса в hello_macro_derive
    Два крейта нужно будет опубликованы отдельно и программисты, использующие эти крейты, должны будут добавить их как зависимости, а затем добавить их в область видимости. Мы могли вместо этого сделать так, что крейт hello_macro использует hello_macro_derive как зависимость и реэкспортирует код процедурного макроса.
    Однако то, как мы структурировали проект, делает возможным программистам использовать hello_macro даже если они не хотят derive функциональность.
    Нам нужно объявить крейт hello_macro_derive как процедурный макрос-крейт. Также понадобятся функционал из крейтов syn и quote
    , как вы увидите через мгновение,
    поэтому нам нужно добавить их как зависимости. Добавьте следующее в файл Cargo.toml
    для hello_macro_derive
    :
    Файл: hello_macro_derive/Cargo.toml
    Чтобы начать определение процедурного макроса, поместите код листинга 19-31 в ваш файл src/lib.rs крейта hello_macro_derive
    . Обратите внимание, что этот код не скомпилируется пока мы не добавим определение для функции impl_hello_macro
    Файл: hello_macro_derive/src/lib.rs
    $
    cargo new hello_macro_derive --lib
    [lib]
    proc-macro = true
    [dependencies]
    syn =
    "1.0"
    quote =
    "1.0"

    Листинг 19-31: Код, который потребуется в большинстве процедурных макро крейтов для обработки Rust
    кода
    Обратите внимание, что мы разделили код на функцию hello_macro_derive
    , которая отвечает за синтаксический анализ
    TokenStream и функцию impl_hello_macro
    , которая отвечает за преобразование синтаксического дерева: это делает написание процедурного макроса удобнее. Код во внешней функции ( hello_macro_derive в данном случае) будет одинаковым для почти любого процедурного макрос крейта, который вы видите или создаёте. Код, который вы указываете в теле внутренней функции (в данном случае impl_hello_macro
    ) будет отличаться в зависимости от цели вашего процедурного макроса.
    Мы представили три новых крейта: proc_macro syn и quote
    . Макрос proc_macro поставляется с Rust, поэтому нам не нужно было добавлять его в зависимости внутри
    Cargo.toml. Макрос proc_macro
    - это API компилятора, который позволяет нам читать и манипулировать Rust кодом из нашего кода.
    Крейт syn разбирает Rust код из строки в структуру данных над которой мы может выполнять операции. Крейт quote превращает структуры данных syn обратно в код
    Rust. Эти крейты упрощают разбор любого вида Rust кода, который мы хотели бы обрабатывать: написание полного синтаксического анализатора для кода Rust не является простой задачей.
    Функция hello_macro_derive будет вызываться, когда пользователь нашей библиотеки указывает своему типу
    #[derive(HelloMacro)]
    . Это возможно, потому что мы аннотировали функцию hello_macro_derive с помощью proc_macro_derive и указали имя
    HelloMacro
    , которое соответствует имени нашего типажа; это соглашение, которому следует большинство процедурных макросов.
    Функция hello_macro_derive сначала преобразует input из
    TokenStream в структуру данных, которую мы можем затем интерпретировать и над которой выполнять операции. Здесь крейт syn вступает в игру. Функция parse в syn принимает
    TokenStream и возвращает структуру
    DeriveInput
    , представляющую разобранный код use proc_macro::TokenStream; use quote::quote; use syn;
    #[proc_macro_derive(HelloMacro)]
    pub fn hello_macro_derive
    (input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate let ast = syn::parse(input).unwrap();
    // Build the trait implementation impl_hello_macro(&ast)
    }

    Rust. Листинг 19-32 показывает соответствующие части структуры
    DeriveInput
    , которые мы получаем при разборе строки struct Pancakes;
    :
    1   ...   51   52   53   54   55   56   57   58   ...   62


    написать администратору сайта