Как я могу сгенерировать границы признаков в декларативном макросе?

У меня есть черта с большим количеством связанных типов. Мне нужна функция, которая использует эти связанные типы по обе стороны от границы предложения where:

      trait Kind {
    type A;
    type B;
    // 20+ more types
}

trait Bound<T> {}

fn example<K1, K2>()
where
    K1: Kind,
    K2: Kind,
    K1::A: Bound<K2::A>,
    K1::B: Bound<K2::B>,
    // 20+ more bounds
{
}

Ввод всех границ будет немного хрупким, поэтому я хотел бы создать макрос, чтобы сгенерировать это:

      fn example<K1, K2>()
where
    K1: Kind,
    K2: Kind,
    how_do_i_write_this!(K1, K2, Bound, [A, B, /* 20+ more types */])
{
}

Однако вызов макроса в правой части привязки предложения where приводит к ошибке:

      macro_rules! bound {
    () => { std::fmt::Debug };
}

fn another_example() 
    where
    u8: bound!(),
{}
      error: expected one of `(`, `+`, `,`, `::`, `;`, `<`, or `{`, found `!`
 --> src/lib.rs:7:14
  |
7 |     u8: bound!(),
  |              ^ expected one of 7 possible tokens

Есть ли какие-нибудь хитрые макросы, которые позволят мне СУШИТЬ этот код?

Я согласен с точным размещением или аргументами изменения макроса. Например, макрос, генерирующий весь fn было бы приемлемо.

Если это невозможно, я могу использовать сценарий сборки, но я бы предпочел, чтобы код оставался совмещенным, если это возможно.

2 ответа

Решение (TL,DR)

  • Макрос, который "испускает" желаемые границы

            macro_rules! with_generated_bounds {( $($rules:tt)* ) => (
        macro_rules! __emit__ { $($rules)* }
        __emit__! {
            K1: Kind,
            K2: Kind,
            K1::A: Bound<K2::A>,
            K1::B: Bound<K2::B>,
            // 20+ more bounds
        }
    )}
    
  • API пользователя (нижестоящего)

     

Объяснение

Это альтернатива ответу sk_pleasant , где они справедливо указывают, что все макросы (включая процедурные, для тех, кому интересно), имеют ограниченное количество разрешенных сайтов для звонков .

  • Самым известным примером этого ограничения является макрос (или любой простой в написании процедурный макрос-полифил, такой): хотя можно расширить макрос до (конкатенированного) идентификатора, вам не разрешено вызывать макрос между fnключевое слово и остальная часть определения функции, что делает бесполезным определение новых функций (и то же ограничение делает такой макрос непригодным для определения новых типов и т. д. ).

    И как люди обходят concat_idents!ограничение? Самый распространенный инструмент / ящик для решения этой проблемы - ::paste, с одноименным макросом.

    Синтаксис макроса особенный. Вместо того, чтобы писать:

            fn
    some_super_fancy_concat_idents![foo, bar]
    (args…)
    { body… }
    

    поскольку, как я уже сказал, это невозможно, ::paste::paste!Идея состоит в том, чтобы вызывать в месте, где разрешены вызовы макросов, например, при расширении до целого элемента , и, таким образом, требовать, чтобы он обертывал все определение функции :

            outer_macro! {
        fn
        /* some special syntax here to signal to `outer_macro!` the intent
           to concatenate the identifiers `foo` and `bar`. */
        (args…)
        { body… }
    }
    

    например ,

            ::paste::paste! {
        fn [< foo bar >] (args…) {
            body…
        }
    }
    

    Когда мы начинаем думать об этом, благодаря внешнему макросу, который видит весь входной «код» как произвольные токены ( не обязательно код Rust!), Мы получаем поддержку воображаемых синтаксисов, таких как, или даже имитации синтаксиса (и подделки!) ) вызовы макросов, но которые на самом деле являются синтаксическим указателем, очень похожим на [< … >]был. То есть API мог быть:

            imaginary::paste! { // <- preprocessor
        // not a real macro call,
        // just a syntactical designator
        // vvvvvvvvvvvvvvvvvvvvvvvv
        fn concat_idents!(foo, bar) (args…) { body… }
    }
    

    Две ключевые идеи во всем этом:

    • Используя внешний вызов, который охватывает все определение функции ( элемент ), мы можем не беспокоиться о сайтах макросов 🙂

    • Мы также можем использовать собственный произвольный синтаксис и правила, такие как псевдомакросы.

    Это основные идеи шаблона препроцессора .

На этом этапе аналогично paste!, можно представить себе процесс-макрос со следующим API:

      my_own_preprocessor! {
    #![define_pseudo_macro(my_bounds := {
        K1: Kind,
        K2: Kind,
        K1::A: Bound<K2::A>,
        K1::B: Bound<K2::B>,
        // 20+ more bounds
    })]

    fn example<K1, K2>()
    where
        K1: Kind,
        K2: Kind,
        my_bounds!() // <- fake macro / syntactical designator for `…preprocessor!`
    …

    trait AnotherExample<K1 : Kind, K2 : Kind>
    where
        my_bounds!() // <- ditto
    {}
}

Это можно было бы сделать, но реализовав вспомогательный proc-macro ( my_own_preprocessor!) нетривиально.


Есть еще один подход, похожий на шаблон препроцессора, но в данном случае его проще реализовать. Это шаблон обратных вызовов, ориентированных на макрос, / стиль продолжения (CPS) . Такой узор в настоящее время появляется время от времени, но он немного громоздкий. Идея состоит в том, что токены, которые мы хотим «испускать», а не испускать, передаются другому макросу - одному, предоставленному вызывающей стороной! - который, в конечном итоге, отвечает за обработку этих токенов и выдачу допустимого расширения макроса - например, куча пунктов / функций - соответственно.

Например, рассмотрите возможность:

      macro_rules! emit_defs {(
    $($bounds:tt)*
) => (
    fn example<K1, K2>()
    where
        K1 : Kind,
        K2 : Kind,
        $($bounds)*
    { … }

    trait AnotherExample<K1 : Kind, K2 : Kind>
    where
        $($bounds)*
    { … }
)}

generate_bounds!(=> emit_defs!);

Если это кажется неудобным, но приемлемым API, то вам следует знать, что реализация основной части generate_bounds!супер тривиально! Действительно, это просто:

      macro_rules! generate_bounds {(
    => $macro_name:ident !
    /* Optionally, we could try to support a fully qualified macro path */
) => (
    $macro_name! {
        K1::A: Bound<K2::A>,
        K1::B: Bound<K2::B>,
        // 20+ more bounds
    }
)}

Сравните это с наивным определением нашего макроса:

      macro_rules! generate_bounds {() => (
    K1::A: Bound<K2::A>,
    K1::B: Bound<K2::B>,
    // 20+ more bounds
)}

Единственное отличие состоит в том, что мы берем макрос (которому будет передано наше возвращенное «значение») в качестве входных данных, и что мы заключаем наш «возвращенный» код в его вызов.

На этом этапе я предлагаю остановиться и взглянуть на предыдущие фрагменты. Концептуальная простота (даже если она зашумлена) и мощность шаблонов на основе обратного вызова часто могут быть выдающимися, и это не исключение!


Это уже довольно хорошее решение, которое иногда можно обнаружить в экосистеме Rust.

Но, имхо, этого недостаточно: эргономика пользователя довольно ужасна. Зачем вызывающей стороне все проблемы, связанные с определением вспомогательного макроса, который может как бы прервать процесс определения функций, которые они хотели определить? И как назвать этот макрос? На самом деле это не имеет значения, это макрос "выстрелил и забыл" "обратного вызова"!

  • Мы достигаем очень схожих проблем с теми, которые должны были определять обратные вызовы в C (даже без сохранения состояния): вместо того, чтобы писать

            with(iterator, |each_element: ElementTy| {
        …
    });
    

    в то время C должен был написать что-то эквивалентное Rust:

            fn handle_element(each_element: ElementTy) {
        …
    }
    
    with(iterator, handle_element);
    

    Сравните это с нашей ситуацией:

            macro_rules! handle_bounds {( $($bounds:tt)* ) => (
        fn example…
        where
            $($bounds)*
        …
    )}
    
    generate_bounds!(=> handle_bounds!);
    

Отсюда довольно легко придумать желаемый API. Что-то вроде:

      with_generated_bounds! {( $($bounds:tt)* ) => (
    fn example…
    where
        $($bounds)*
    …
)}

И использование этого API из «именованного обратного вызова» (того самого) на самом деле довольно прямолинейно: если мы посмотрим на два предыдущих фрагмента, мы можем заметить, что «обратный вызов», предоставленный вызывающим, является в точности телом macro_rules! определение.

Таким образом, мы можем сами определить «вспомогательный» макрос (вызываемый) с помощью правил, предоставленных вызывающим, а затем вызвать этот вспомогательный макрос для кода, который мы хотели испустить.

Это приводит к решению, описанному в начале этого поста (повторено для удобства 🙃):

  • Макрос, который "испускает" желаемые границы

            macro_rules! with_generated_bounds {( $($rules:tt)* ) => (
        /// The helper "callback" macro
        macro_rules! __emit__ { $($rules)* }
    
        __emit__! {
            K1: Kind,
            K2: Kind,
            K1::A: Bound<K2::A>,
            K1::B: Bound<K2::B>,
            // 20+ more bounds
        }
    )}
    
  • API пользователя (нижестоящего)

            with_generated_bounds! {( $($bounds:tt)* ) => (
        fn example<K1, K2>()
        where
            K1 : Kind,
            K2 : Kind,
            $($bounds)*
        { … }
    
        trait AnotherExample<K1 : Kind, K2 : Kind>
        where
            $($bounds)*
        { … }    
    )}
    

И вуаля 🙂

Как использовать этот шаблон при использовании фактических аргументов макроса?

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

  • Пользовательский API будет примерно таким:

            with_bounds_for! { K1, K2, ( $($bounds:tt)* ) => (
        fn example<K1, K2>()
        where
            $($bounds)*
        …
    )}
    
  • Макрос встроенного callback-шаблона тогда будет:

            macro_rules! with_bounds_for {(
        $K1:ident, $K2:ident, $($rules:tt)*
    ) => (
        macro_rules! __emit__ { $($rules)* }
        __emit__! {
            $K1 : Kind,
            $K2 : Kind,
            …
        }
    )}
    

Некоторые замечания

Обратите внимание, что расширение with_generated_bounds! это из:

  • определение макроса;

  • вызов макроса.

Это два «оператора», что означает, что все раскрытие макроса является самим «оператором», а это означает, что следующее не будет работать:

      macro_rules! with_42 {( $($rules:tt)* ) => (
    macro_rules! __emit__ { $($rules)* }
    __emit__! { 42 }
)}

//      this macro invocation expands to two "statements";
//      it is thus a statement / `()`-evaluating expression itself
//      vvvvvvvvvv
let x = with_42! {( $ft:expr ) => (
    $ft + 27
)};

Это nihil novi sub sole / ничего нового под солнцем; это та же проблема, что и с:

      macro_rules! example {() => (
    let ft = 42; // <- one "statement"
    ft + 27      // <- an expression
)}

let x = example!(); // Error

И в этом случае решение простое: заключите операторы в фигурные скобки, чтобы создать блок , который, таким образом, может вычислить свое последнее выражение:

      macro_rules! example {() => ({
    let ft = 42;
    ft + 27
})}
  • (Кстати, по этой причине я предпочитаю использовать => ( … ) как правая часть macro rules; он менее подвержен ошибкам / пулеметам, чем => { … }).

В этом случае то же самое решение применяется к шаблону обратного вызова:

      macro_rules! with_ft {( $($rules:tt)* ) => ({
    macro_rules! __emit__ { $($rules)* }
    __emit__! { 42 }
})}
// OK
let x = with_ft! {( $ft:expr ) => (
    $ft + 27
)};

Это делает макрос expr-дружелюбно, но за счет того, что приводит к блокировке определений элементов:

      // Now the following fails!
with_ft! {( $ft:expr ) => (
    fn get_ft() -> i32 {
        $ft
    }
)}
get_ft(); // Error, no `get_ft` in this scope

Действительно, определение get_ft теперь была ограничена фигурными скобками 😕

Таким образом, это основное ограничение встроенного / анонимного шаблона обратного вызова: хотя он достаточно мощный, чтобы имитировать «произвольные расширения» и «произвольные сайты вызовов», он ограничен необходимостью заранее выбрать, будет ли он заключать определение макроса в фигурные скобки. block или нет, что делает его совместимым либо с макросами расширения выражений, либо с макросами расширения общедоступных элементов. В этой связи несколько более громоздким назвали модель обратного вызова, показана в середине этого поста ( => macro_name! синтаксис) не имеет этой проблемы.

Цитата из справочника по макросам Rust :

Макросы могут быть вызваны в следующих ситуациях:

  • Выражения и утверждения
  • Узоры
  • Типы
  • Элементы, включая связанные элементы
  • транскриберы macro_rules
  • Внешние блоки

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

      trait Kind {
    type A;
    type B;
    // 20+ more types
}

trait Bound<T> {}

macro_rules! generate_func_with_bounds {
    (
        fn $name:ident <$($gens:ident),*> ()
        where
            $($bound_type:ident: $bound_to:ident),*,
            @generate_bounds($first_type:ident, $second_type:ident, $trait:ident, [$($assoc:ident),*])
        {
            $($body:tt)*
        }
    ) => {
        fn $name <$($gens),*> ()
        where
            $($bound_type: $bound_to),*,
            $($first_type::$assoc: $trait<$second_type::$assoc>),*
        {
            $($body)*
        }
    };
}

generate_func_with_bounds!{
    fn example<K1, K2>()
    where
        K1: Kind,
        K2: Kind,
        @generate_bounds(K1, K2, Bound, [A, B])
    {
    }
}

Детская площадка

У этого есть подпись, которую вы хотите. Обратите внимание, что вам может потребоваться немного изменить сопоставление, если вы хотите, чтобы это работало с другими функциями (например, функциями с параметрами, функциями, которые используют общие времена жизни и т. Д. - все, что не более или менее синтаксически эквивалентно example() декларация).

Другие вопросы по тегам