[英]Rust macro to format arguments over multiple formats
TL;博士
我正在嘗試編寫一個將執行以下轉換的宏:
magic_formatter!(["_{}", "{}_", "_{}_"], "foo") ==
[format!("_{}", "foo"),
format!("{}_", "foo"),
format!("_{}_", "foo")]
(也歡迎提供["_foo", "foo_", "_foo_"]
並適用於可變參數的解決方案)
全文:
我正在寫一個解析器,它的許多測試都是這樣的:
let ident = identifier().parse("foo ").unwrap();
assert_eq!(ident, Syntax::ident("foo"));
let ident = identifier().parse(" foo").unwrap();
assert_eq!(ident, Syntax::ident("foo"));
let ident = identifier().parse(" foo ").unwrap();
assert_eq!(ident, Syntax::ident("foo"));
所以我試圖通過這樣做來減少重復:
for f in [" {}", "{} ", " {} "] {
let inp = format!(f, "foo");
let ident = identifier().parse(inp).unwrap();
assert_eq!(ident, Syntax::ident("foo"));
}
這當然不會編譯。
但是,在我看來,實際上並沒有任何未知信息阻止在編譯時生成整個數組,所以我搜索了 webz,希望這已經在某個地方解決了,但是我的 google-fu 不能似乎找到了任何我想要的東西。
所以我想我會弄臟我的手,第一次寫一個真正有用的 rust 宏(!)。
我通過示例閱讀了Rust的宏章節,並失敗了一段時間。 然后我嘗試閱讀實際參考資料,我覺得這讓我更進一步,但我仍然無法正確理解。 然后我真的進入它並找到了這個很酷的解釋並認為我這次真的有它,但我似乎仍然無法讓我的宏正常工作並同時編譯。
我最近的嘗試是這樣的:
macro_rules! map_fmt {
(@accum () -> $($body:tt),*) => { map_fmt!(@as_expr [$($body),*]) };
(@accum ([$f:literal, $($fs:literal),*], $args:tt) -> $($body:tt),*) => {
map_fmt!(@accum ([$($fs),*], $args) -> (format!($f, $args) $($body),*))
};
(@as_expr $e:expr) => { $e };
([$f:literal, $($fs:literal),*], $args:expr) => {
map_fmt!(@accum ([$f, $($fs),*], $args) -> ())
};
}
如果有人可以幫助我了解我的宏缺少什么,我將不勝感激? 以及如何解決它,如果可能的話? 如果沒有,我可以/應該使用其他一些技術來減少測試中的重復嗎?
編輯:
這是我使用的最終解決方案,這是@finomnis 提供的正確答案,我對其進行了輕微修改以支持format!
表達
macro_rules! map_fmt {
(@accum ([$f:literal], $($args:tt),*) -> ($($body:tt)*)) => { [$($body)* format!($f, $($args),*)] };
(@accum ([$f:literal, $($fs:literal),*], $($args:tt),*) -> ($($body:tt)*)) => {
map_fmt!(@accum ([$($fs),*], $($args),*) -> ($($body)* format!($f, $($args),*),))
};
([$f:literal, $($fs:literal),*], $($args:expr),*) => {
map_fmt!(@accum ([$f, $($fs),*], $($args),*) -> ())
};
}
format!()
不起作用,因為它在編譯時生成代碼,因此需要一個實際的字符串文字格式化程序。
然而, str::replace()
有效:
fn main() {
for f in [" {}", "{} ", " {} "] {
let inp = f.replace("{}", "foo");
println!("{:?}", inp);
}
}
" foo"
"foo "
" foo "
我不認為在運行時這樣做是一個問題,特別是你的format!()
宏中的調用也是運行時替換,但我認為這是了解更多關於宏的有趣挑戰。
你的宏有幾個問題。
一方面, ()
案例應該是([], $_:tt)
。
但是您的宏的主要問題是[$f:literal, $($fs:literal),*]
不匹配[""]
(只剩下一個文字的情況),因為它與所需的不匹配逗號。 這將匹配: ["",]
。 這可以通過將$(),*
轉換為$(),+
來解決(意思是,它們必須攜帶至少一個元素),然后用[$f:literal]
替換[]
(沒有剩余元素)的情況(剩下一個元素)。 然后處理只剩下一個元素並且逗號不匹配的特殊情況。
您選擇中間結果的方式在幾個地方存在小錯誤。 在某些地方,您忘記了它周圍的()
,並且參數的順序可能錯誤。 此外,最好將它們傳輸為$(tt)*
而不是$(tt),*
,因為tt
已經包含逗號。
根據較新的宏 book ,您的$as_expr
案例沒有多大用途,因此我將其刪除。
這就是你的代碼在修復所有這些東西后的樣子:
macro_rules! map_fmt {
(@accum ([$f:literal], $args:tt) -> ($($body:tt)*)) => {
[$($body)* format!($f, $args)]
};
(@accum ([$f:literal, $($fs:literal),*], $args:tt) -> ($($body:tt)*)) => {
map_fmt!(@accum ([$($fs),*], $args) -> ($($body)* format!($f, $args),))
};
([$f:literal, $($fs:literal),*], $args:expr) => {
map_fmt!(@accum ([$f, $($fs),*], $args) -> ())
};
}
fn main() {
let fmt = map_fmt!(["_{}", "{}_", "_{}_"], "foo");
println!("{:?}", fmt);
}
["_foo", "foo_", "_foo_"]
但是,如果您使用cargo expand
來打印宏解析的內容,您會得到以下結果:
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
let fmt = [
{
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["_"],
&[::core::fmt::ArgumentV1::new_display(&"foo")],
));
res
},
{
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["", "_"],
&[::core::fmt::ArgumentV1::new_display(&"foo")],
));
res
},
{
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["_", "_"],
&[::core::fmt::ArgumentV1::new_display(&"foo")],
));
res
},
];
{
::std::io::_print(::core::fmt::Arguments::new_v1(
&["", "\n"],
&[::core::fmt::ArgumentV1::new_debug(&fmt)],
));
};
}
您可以在這里清楚地看到format!
仍然是運行時調用。 所以我不認為宏實際上創造了任何加速。
你可以用const_format
crate 解決這個問題:
macro_rules! map_fmt {
(@accum ([$f:literal], $args:tt) -> ($($body:tt)*)) => {
[$($body)* ::const_format::formatcp!($f, $args)]
};
(@accum ([$f:literal, $($fs:literal),*], $args:tt) -> ($($body:tt)*)) => {
map_fmt!(@accum ([$($fs),*], $args) -> ($($body)* ::const_format::formatcp!($f, $args),))
};
([$f:literal, $($fs:literal),*], $args:expr) => {{
map_fmt!(@accum ([$f, $($fs),*], $args) -> ())
}};
}
fn main() {
let fmt = map_fmt!(["_{}", "{}_", "_{}_"], "foo");
println!("{:?}", fmt);
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}
print_type_of(&fmt);
}
["_foo", "foo_", "_foo_"]
[&str; 3]
您現在可以看到類型是&'static str
,這意味着它現在在編譯時被格式化並作為靜態字符串存儲在二進制文件中。
綜上所述,我認為宏中的整個遞歸是毫無意義的。 似乎可以通過一次重復來完成:
macro_rules! map_fmt {
([$($fs:literal),*], $args:expr) => {{
[$(format!($fs, $args)),*]
}};
}
fn main() {
let fmt = map_fmt!(["_{}", "{}_", "_{}_"], "foo");
println!("{:?}", fmt);
}
["_foo", "foo_", "_foo_"]
如果你想支持format!()
的任意數量的參數,那么你可以這樣做:
macro_rules! map_fmt {
(@format $f:literal, ($($args:expr),*)) => {
format!($f, $($args),*)
};
([$($fs:literal),*], $args:tt) => {{
[$(map_fmt!(@format $fs, $args)),*]
}};
}
fn main() {
let fmt = map_fmt!(["_{}_{}", "{}__{}", "{}_{}_"], ("foo", "bar"));
println!("{:?}", fmt);
}
["_foo_bar", "foo__bar", "foo_bar_"]
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.