macro_rules!
宏实现的解决方案
使用声明式宏(macro_rules!
)实现这个功能有点棘手,但是可以做到。但是,需要使用一些技巧。
首先,这里是代码 (Playground):
macro_rules! replace {
($x:ident, $y:ident, $($e:tt)*) => {
replace!(@impl $x, $y, [], $($e)*)
};
(@impl $x:ident, $y:ident, [$($out:tt)*], ) => {
$($out)*
};
(@impl $x:ident, $y:ident, [$($out:tt)*], $head:ident $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* replace!(@replace $x $y $head)],
$($tail)*
)
}};
(@impl $x:ident, $y:ident, [$($out:tt)*], ( $($head:tt)* ) $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* ( replace!($x, $y, $($head)*) ) ],
$($tail)*
)
}};
(@impl $x:ident, $y:ident, [$($out:tt)*], [ $($head:tt)* ] $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* [ replace!($x, $y, $($head)*) ] ],
$($tail)*
)
}};
(@impl $x:ident, $y:ident, [$($out:tt)*], { $($head:tt)* } $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* { replace!($x, $y, $($head)*) } ],
$($tail)*
)
}};
(@impl $x:ident, $y:ident, [$($out:tt)*], $head:tt $($tail:tt)*) => {{
replace!(@impl $x, $y, [$($out)* $head], $($tail)*)
}};
(@replace $needle:ident $replacement:ident $i:ident) => {{
macro_rules! __inner_helper {
($needle $needle) => { $replacement };
($needle $i) => { $i };
}
__inner_helper!($needle $i)
}}
}
fn main() {
let foo = 3;
let bar = 7;
let z = 5;
dbg!(replace!(abc, foo, bar * 100 + z));
dbg!(replace!(bar, foo, bar * 100 + z));
}
它的输出结果为:
[src/main.rs:56] replace!(abc , foo , bar * 100 + z) = 705
[src/main.rs:57] replace!(bar , foo , bar * 100 + z) = 305
这是如何工作的?
在理解这个宏之前,有两个主要的技巧需要理解:
向下累积和
如何检查两个标识符是否相等。
此外,为了确保:宏模式开头的
@foobar
不是特殊功能,而只是一种约定,用于标记内部帮助宏(还请参见:
"Macros小书",
StackOverflow问题)。
向下累积在
"Rust宏小书"的这一章节中有很好的描述。重要部分如下:
Rust中的所有宏必须产生完整、受支持的语法元素(例如表达式、项等)。这意味着无法将宏扩展为部分结构。
但是,在处理逐个令牌时通常需要部分结果。为了解决这个问题,基本上有一个“输出”参数,它只是一个随着每次递归宏调用而增长的令牌列表。这是可行的,因为宏输入可以是任意令牌,不必是有效的Rust结构。
这种模式只对作为“增量TT咀嚼器”的宏有意义,而我的解决方案就是如此。还有
TLBORM中关于这个模式的一章。
第二个关键点是
检查两个标识符是否相等。这是通过一个有趣的技巧来完成的:宏定义一个新宏,然后立即使用它。让我们看一下代码:
(@replace $needle:ident $replacement:ident $i:ident) => {{
macro_rules! __inner_helper {
($needle $needle) => { $replacement };
($needle $i) => { $i };
}
__inner_helper!($needle $i)
}}
让我们来看两种不同的调用:
replace!(@replace foo bar baz)
: this expands to:
macro_rules! __inner_helper {
(foo foo) => { bar };
(foo baz) => { baz };
}
__inner_helper!(foo baz)
And the inner_helper!
invocation now clearly takes the second pattern, resulting in baz
.
replace!(@replace foo bar foo)
on the other hand expands to:
macro_rules! __inner_helper {
(foo foo) => { bar };
(foo foo) => { foo };
}
__inner_helper!(foo foo)
This time, the inner_helper!
invocation takes the first pattern, resulting in bar
.
我从一个仅提供检查两个标识符是否相等的宏中学到了这个技巧。但不幸的是,我找不到这个宏包了。如果您知道那个宏包的名称,请告诉我!
然而,这种实现有一些限制:
使用proc-macro的解决方案
当然,也可以通过proc-macro来实现。这也涉及到较少的奇怪技巧。我的解决方案如下:
extern crate proc_macro;
use proc_macro::{
Ident, TokenStream, TokenTree,
token_stream,
};
#[proc_macro]
pub fn replace(input: TokenStream) -> TokenStream {
let mut it = input.into_iter();
let needle = get_ident(&mut it);
let _comma = it.next().unwrap();
let replacement = get_ident(&mut it);
let _comma = it.next().unwrap();
it.map(|tt| {
match tt {
TokenTree::Ident(ref i) if i.to_string() == needle.to_string() => {
TokenTree::Ident(replacement.clone())
}
other => other,
}
}).collect()
}
fn get_ident(it: &mut token_stream::IntoIter) -> Ident {
match it.next() {
Some(TokenTree::Ident(i)) => i,
_ => panic!("oh noes!"),
}
}
使用这个过程宏与上面的
main()
示例完全相同。
注意:错误处理在此处被忽略以使示例简短。请参见
此问题关于如何在过程宏中报告错误。
除此之外,那段代码不需要太多解释,我认为。这个过程宏版本也不会像
macro_rules!
宏一样受到递归限制的问题的困扰。
macro_rules!
来解决这个问题,因为它是最有文档记录的。但是我在那一点上卡住了,找不到一种匹配的方法。我应该分享我的尝试吗? - hoheinzollern