Как я могу сгенерировать границы признаков в декларативном макросе?
У меня есть черта с большим количеством связанных типов. Мне нужна функция, которая использует эти связанные типы по обе стороны от границы предложения 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()
декларация).