解决方案 (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
with_generated_bounds! {( $($bounds:tt)* ) => (
fn example<K1, K2>()
where
K1 : Kind,
K2 : Kind,
$($bounds)*
{ … }
trait AnotherExample<K1 : Kind, K2 : Kind>
where
$($bounds)*
{ … }
)}
解释
这是sk_pleasant 的答案的替代方案,他们正确地指出,所有宏(包括程序宏,对于那些想知道的人)都有有限数量的允许调用站点。
这种限制最著名的例子是concat_idents!
宏(或任何易于编写的过程宏 polyfill):虽然可以将宏扩展为(连接的)标识符,但不允许在fn
关键字之间调用宏和函数定义的其余部分,因此concat_idents!
无法定义新函数(同样的限制使得这样的宏无法用于定义新类型等)。
人们如何规避concat_idents!
限制?解决这个问题的最广泛的工具/板条箱是::paste
使用同名的宏。
宏的语法很特殊。而不是写:
fn
some_super_fancy_concat_idents![foo, bar]
(args…)
{ body… }
因为,正如我所提到的,这是不可能的,::paste::paste!
'''''''''''''''''''''''''''''''''''''''''''''''''''''0000000000000004想法的想法的想法是在允许宏允许被允许的想法的想法是在扩展为允许扩展到整个函数的想法是在扩展到整个函数时的想法是可能的想法的想法的想法的想法的想法的想法的想法的想法的想法的想法的想法的想法的地方的的地方的的是在扩展的地方的地方的地方的地方在扩展到整个函数的函数的函数的函数的函数。
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 代码!),我们可以支持想象的语法,例如[< … >]
,甚至是模仿语法(和伪造! ) 宏调用,但实际上只是一个句法指示符,很像[< … >]
was。也就是说,paste!
的 API 可能是:
imaginary::paste! { // <- preprocessor
// not a real macro call,
// just a syntactical designator
// vvvvvvvvvvvvvvvvvvvvvvvv
fn concat_idents!(foo, bar) (args…) { body… }
}
整个事情的两个关键思想是:
这些是预处理器模式的核心思想。
此时,类似于paste!
,可以设想具有以下 API 的 proc-macro 方法:
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)*
…
)}
并且从“命名回调”一(the => macro_name!
one)中使用这个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)*
…
)}
内联回调模式宏将是:
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
在这种情况下,解决方案很简单:将语句包装在大括号中,以便发出一个block,从而可以计算出它的最后一个表达式:
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
事实上,of 的定义get_ft
现在被限定在大括号内
因此,这是内联/匿名回调模式的主要限制:虽然它足以模拟“任意扩展”和“任意调用站点”,但它仅限于必须事先选择是否将宏定义包装在一个大括号内是否阻止,这使得它与表达式扩展宏或公共项目扩展宏兼容。在这方面,本文中间介绍的稍微麻烦的命名回调模式(=> macro_name!
语法)没有这个问题。