在宏中使用可选和非可选参数压缩迭代
Zip iterables with Optional and Non Optional parameter in macro
对于我的词法分析器的测试部分,我想出了一个简单的宏,让 met 定义预期的标记类型(枚举)和标记文字(字符串):
macro_rules! token_test {
($($ttype:ident: $literal:literal)*) => {
{
vec!($($ttype,)*).iter().zip(vec!($($literal,)*).iter())
}
}
}
然后我可以这样使用它:
for (ttype, literal) in token_test! {
Let: "let" Identifier: "five" Assign: "=" Int: "5" Semicolon: ";"
} {
//...
}
然而,这有点冗长,我们不需要为大部分标记指定文字,因为我有另一个宏将枚举变体转换为字符串(例如:让-> "让").
所以我希望做的是:
for (ttype, literal) in token_test! {
Let Identifier: "five" Assign Int: "5" Semicolon
} {
//...
}
如果我理解正确,我可以使用可选参数来匹配 TYPE: LITERAL
或 TYPE
。也许是这样的:
macro_rules! token_test {
($($ttype:ident$(: $literal:literal)?)*) => {
{
//...
}
}
}
那么我的问题是有没有办法从中构建 Vector
?
更清楚:
- 在没有传递文字的情况下,它应该添加我的枚举的字符串表示(例如:Let -> “let”)
- 传字面量的情况下,直接加上字面量
使其与以下宏一起工作(欢迎任何改进):
macro_rules! token_test {
($($ttype:ident$(: $literal:literal)?)*) => {
vec!($($ttype,)*).iter().zip(vec!(
$(
{
let mut literal = $ttype.as_str().unwrap();
$(literal = $literal;)?
literal
}
),*).iter())
}
}
这个 'iterates' 在 literal
宏参数上,并最初设置 as_str
的值,它将枚举变体转换为字符串。然后,如果定义了 $literal
,它会将本地文字值替换为该值。最后,它 returns 局部文字变量。
改进
macro_rules! some_or_none {
() => { None };
($entity:literal) => { Some($entity) }
}
macro_rules! token_test {
($($ttype:ident$(: $literal:literal)?)*) => {
vec!($($ttype,)*).iter().zip(vec!($(
some_or_none!($($literal)?).unwrap_or($ttype.as_str().unwrap())
),*))
}
}
删除了一些不必要的范围,第二个 .iter()
,并添加了 some_or_none
宏。通过这种方式,如果提供了文字,我不需要执行 as_str
。
进一步改进
在上面的例子中,提供了两个宏。一个显然是一个“私有”宏,因为它的存在只对另一个的执行有用。但是,关于宏导出的工作原理有一个小问题。与函数不同,宏无法访问在同一作用域中定义但调用者无法访问的宏。参见 this playground example。如果您不打算导出该宏,这不是问题,这是可能的,因为它的唯一目的是在测试套件中使用。但是,您可能仍希望在 crate 级别公开公开它,而不公开 some_or_none!
。执行此操作的常规方法是将 some_or_none!
集成到 token_test!
宏中,方法是在其前面添加 @
:
macro_rules! token_test {
(@some_or_none) => {
None
};
(@some_or_none $entity:literal) => {
Some($entity)
};
($($ttype:ident $(: $literal:literal)?)*) => {
vec!($($ttype,)*)
.iter()
.zip(vec!($(
token_test!(@some_or_none $($literal)?)
.unwrap_or($ttype.as_str().unwrap())
),*))
};
}
使用此版本,您可以安全地将 test_token
导出为 shown in this playground。
多一点
- 来自 Rust 论坛 steffahn 的原创想法
还有另一种类似的方法可以解决这个问题并且不涉及 unwrap_or
,而不是在 some_or_none
中包装成 Option
,我们实际上可以创建两个分支,它们采用 [= =30=] 或 TYPE
,像这样:
macro_rules! token_test {
(@ttype_or_literal $ttype:ident) => { $ttype.as_str().unwrap() };
(@ttype_or_literal $ttype:ident: $literal:literal) => { $literal };
($($ttype:ident $(: $literal:literal)?)*) => {
vec!($($ttype,)*)
.iter()
.zip(vec![$(token_test!(@ttype_or_literal $ttype$(: $literal)?)),*])
};
}
再一次
因为我只需要一个可以解构为(type, iterable)
的可迭代对象,一对数组就足够了:
macro_rules! token_test {
(@ttype_or_literal $ttype:ident) => { $ttype.as_str().unwrap() };
(@ttype_or_literal $ttype:ident: $literal:literal) => { $literal };
($($ttype:ident $(: $literal:literal)?)*) => {
[$(($ttype, token_test!(@ttype_or_literal $ttype$(: $literal)?))),*]
};
}
所以不再 vec
也不再 zip
.
聪明的把戏
A user on the Rust forum 给出了这个潜在的技巧,涉及忽略第二个参数(如果存在)。我通过没有两个宏使解决方案更紧凑:
macro_rules! token_test {
(@ignore_second $value:expr $(, $_ignored:expr)? $(,)?) => { $value };
($($ttype:ident $(: $literal:literal)?)*) => {
[$(($ttype, token_test!(@ignore_second $($literal,)? $ttype.as_str().unwrap()))),*]
};
}
对于我的词法分析器的测试部分,我想出了一个简单的宏,让 met 定义预期的标记类型(枚举)和标记文字(字符串):
macro_rules! token_test {
($($ttype:ident: $literal:literal)*) => {
{
vec!($($ttype,)*).iter().zip(vec!($($literal,)*).iter())
}
}
}
然后我可以这样使用它:
for (ttype, literal) in token_test! {
Let: "let" Identifier: "five" Assign: "=" Int: "5" Semicolon: ";"
} {
//...
}
然而,这有点冗长,我们不需要为大部分标记指定文字,因为我有另一个宏将枚举变体转换为字符串(例如:让-> "让").
所以我希望做的是:
for (ttype, literal) in token_test! {
Let Identifier: "five" Assign Int: "5" Semicolon
} {
//...
}
如果我理解正确,我可以使用可选参数来匹配 TYPE: LITERAL
或 TYPE
。也许是这样的:
macro_rules! token_test {
($($ttype:ident$(: $literal:literal)?)*) => {
{
//...
}
}
}
那么我的问题是有没有办法从中构建 Vector
?
更清楚:
- 在没有传递文字的情况下,它应该添加我的枚举的字符串表示(例如:Let -> “let”)
- 传字面量的情况下,直接加上字面量
使其与以下宏一起工作(欢迎任何改进):
macro_rules! token_test {
($($ttype:ident$(: $literal:literal)?)*) => {
vec!($($ttype,)*).iter().zip(vec!(
$(
{
let mut literal = $ttype.as_str().unwrap();
$(literal = $literal;)?
literal
}
),*).iter())
}
}
这个 'iterates' 在 literal
宏参数上,并最初设置 as_str
的值,它将枚举变体转换为字符串。然后,如果定义了 $literal
,它会将本地文字值替换为该值。最后,它 returns 局部文字变量。
改进
macro_rules! some_or_none {
() => { None };
($entity:literal) => { Some($entity) }
}
macro_rules! token_test {
($($ttype:ident$(: $literal:literal)?)*) => {
vec!($($ttype,)*).iter().zip(vec!($(
some_or_none!($($literal)?).unwrap_or($ttype.as_str().unwrap())
),*))
}
}
删除了一些不必要的范围,第二个 .iter()
,并添加了 some_or_none
宏。通过这种方式,如果提供了文字,我不需要执行 as_str
。
进一步改进
在上面的例子中,提供了两个宏。一个显然是一个“私有”宏,因为它的存在只对另一个的执行有用。但是,关于宏导出的工作原理有一个小问题。与函数不同,宏无法访问在同一作用域中定义但调用者无法访问的宏。参见 this playground example。如果您不打算导出该宏,这不是问题,这是可能的,因为它的唯一目的是在测试套件中使用。但是,您可能仍希望在 crate 级别公开公开它,而不公开 some_or_none!
。执行此操作的常规方法是将 some_or_none!
集成到 token_test!
宏中,方法是在其前面添加 @
:
macro_rules! token_test {
(@some_or_none) => {
None
};
(@some_or_none $entity:literal) => {
Some($entity)
};
($($ttype:ident $(: $literal:literal)?)*) => {
vec!($($ttype,)*)
.iter()
.zip(vec!($(
token_test!(@some_or_none $($literal)?)
.unwrap_or($ttype.as_str().unwrap())
),*))
};
}
使用此版本,您可以安全地将 test_token
导出为 shown in this playground。
多一点
- 来自 Rust 论坛 steffahn 的原创想法
还有另一种类似的方法可以解决这个问题并且不涉及 unwrap_or
,而不是在 some_or_none
中包装成 Option
,我们实际上可以创建两个分支,它们采用 [= =30=] 或 TYPE
,像这样:
macro_rules! token_test {
(@ttype_or_literal $ttype:ident) => { $ttype.as_str().unwrap() };
(@ttype_or_literal $ttype:ident: $literal:literal) => { $literal };
($($ttype:ident $(: $literal:literal)?)*) => {
vec!($($ttype,)*)
.iter()
.zip(vec![$(token_test!(@ttype_or_literal $ttype$(: $literal)?)),*])
};
}
再一次
因为我只需要一个可以解构为(type, iterable)
的可迭代对象,一对数组就足够了:
macro_rules! token_test {
(@ttype_or_literal $ttype:ident) => { $ttype.as_str().unwrap() };
(@ttype_or_literal $ttype:ident: $literal:literal) => { $literal };
($($ttype:ident $(: $literal:literal)?)*) => {
[$(($ttype, token_test!(@ttype_or_literal $ttype$(: $literal)?))),*]
};
}
所以不再 vec
也不再 zip
.
聪明的把戏
A user on the Rust forum 给出了这个潜在的技巧,涉及忽略第二个参数(如果存在)。我通过没有两个宏使解决方案更紧凑:
macro_rules! token_test {
(@ignore_second $value:expr $(, $_ignored:expr)? $(,)?) => { $value };
($($ttype:ident $(: $literal:literal)?)*) => {
[$(($ttype, token_test!(@ignore_second $($literal,)? $ttype.as_str().unwrap()))),*]
};
}