我有一个具有大量关联类型的特征。我想要一个在 where 子句绑定(bind)的两侧使用这些关联类型的函数:
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 子句绑定(bind)的右侧调用宏会导致错误: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
可以接受。如果这是不可能的,我可以使用构建脚本,但如果可能,我宁愿将代码放在同一位置。
最佳答案
解决方案(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
}
)}
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's answer 的替代方案,他们正确地指出所有宏(包括程序宏,对于那些想知道的人),都有 a limited amount of allowed call sites .
concat_idents!
macro (或任何易于编写的程序宏 polyfill):虽然可以将宏扩展为(连接的)标识符,但不允许在 fn
之间调用宏。关键字和函数定义的其余部分,从而使 concat_idents!
用于定义新函数(同样的限制使得这样的宏无法用于定义新类型等)。以及人们如何规避
concat_idents!
局限性?解决这个问题的最普遍的工具/ crate 是 ::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 代码!),我们开始支持想象的语法,例如 [< … >]
,甚至是模仿(和伪造!)宏调用的语法,但实际上只是一个语法指示符,很像 [< … >]
曾是。即,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
{}
}
这可以做到,但实现辅助程序宏( my_own_preprocessor!
)并非易事。还有另一种类似于预处理器模式的方法,但在这种情况下,它更容易特征化。它是 宏观目标
回调/继续传递风格 (CPS) 图案 .这种模式目前不时出现,但有点麻烦。这个想法是,我们希望“发出”而不是发出的 token 被传递给另一个宏——一个由调用者提供的!——它最终负责处理这些 token 并发出有效的宏扩展——例如一堆项目/功能——相应地。
例如,考虑做:
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!
的主体 super 简单!确实,它只是: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
)}
唯一的区别是我们将一个宏(它将被提供给我们返回的“值”)作为输入,并且我们将我们的“返回”代码包装在它的调用中。在这一点上,我建议暂停并凝视前面的片段。基于回调的模式的概念简单(即使嘈杂)和强大的功能通常非常出色,这也不异常(exception)!
这已经很好了,并且已经是一个有时可以在 Rust 生态系统中发现的解决方案。
但是,恕我直言,这还不够好:用户的人体工程学非常糟糕。为什么调用者要经历定义帮助宏的所有麻烦,这可能会中断定义他们想要定义的函数的流程?那个宏应该如何命名?真的无所谓,火了忘了“回调”宏!
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)*
…
)}
并且从“命名回调”一个(=> macro_name!
一个)中使用这个 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
}
)}
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
.将这些作为参数怎么样?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鞋底/太阳底下无新事;这与以下问题相同: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
的定义现在范围在大括号内 😕因此,这是内联/匿名回调模式的主要限制:虽然它足以模拟“任意扩展”和“任意调用点”,但仅限于必须事先选择是否将宏定义包装在一个大括号内阻止与否,这使其与表达式扩展宏或公共(public)项目扩展宏兼容。在这方面,本文中间介绍的稍微麻烦的命名回调模式(
=> macro_name!
语法)没有这个问题。
关于rust - 如何在声明性宏中生成特征边界?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68728105/