Cleaner Match Arms for Deeply Nested Enums - rust

I have the following function with a match expression:
fn handle_event<'e>(&mut self, event: Event<'e>) -> Event<'e> {
match (&event, &self.current_lang) {
(Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))), _) => {
self.start_fenced_code_block(&lang)
}
(Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(_))), _) => {
self.end_fenced_code_block()
}
(Event::Text(text), Some(lang)) => self.code_html(&text, &lang),
_ => event,
}
}
However, the first two arms felt like they were getting out of hand due to deeply nested enums. So I made some macros:
macro_rules! fenced_code_block_start {
($lang:pat_param) => {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced($lang)))
};
}
macro_rules! fenced_code_block_end {
() => {
Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(_)))
};
}
and now I have the, IMHO, cleaner:
match (&event, &self.current_lang) {
(fenced_code_block_start!(lang), _) => self.start_fenced_code_block(&lang),
(fenced_code_block_end!(), _) => self.end_fenced_code_block(),
(Event::Text(text), Some(lang)) => self.code_html(&text, &lang),
_ => event,
}
However, I'm wondering if there's a better way to do this. Dropping down into macros always give me pause. Is there some other feature of Rust I can use here that isn't such a heavy hammer?

If clarity isn't lost, you can import and use the enum variants directly:
fn handle_event<'e>(&mut self, event: Event<'e>) -> Event<'e> {
use Event::*;
use Tag::*;
use CodeBlockKind::*;
match (&event, &self.current_lang) {
(Start(CodeBlock(Fenced(lang))), _) => self.start_fenced_code_block(&lang),
(End(CodeBlock(Fenced(_))), _) => self.end_fenced_code_block(),
(Text(text), Some(lang)) => self.code_html(&text, &lang),
_ => event,
}
}

Related

Rust macro to format arguments over multiple formats

TL;DR
I'm trying to write a macro that will do the following transformation:
magic_formatter!(["_{}", "{}_", "_{}_"], "foo") ==
[format!("_{}", "foo"),
format!("{}_", "foo"),
format!("_{}_", "foo")]
(a solution that will give ["_foo", "foo_", "_foo_"] and works for varargs is also welcome)
Full story:
I'm writing a parser, and many of it's tests do stuff like this:
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"));
so I tried to reduce repetition by doing this:
for f in [" {}", "{} ", " {} "] {
let inp = format!(f, "foo");
let ident = identifier().parse(inp).unwrap();
assert_eq!(ident, Syntax::ident("foo"));
}
which of course doesn't compile.
However, it seems to me that there isn't really any unknown information preventing from the whole array to be generated at compile time, so I searched the webz, hoping that this has been solved somewhere already, but my google-fu can't seem to find anything that just does what I want.
So I thought I'd get my hands dirty and write an actually useful rust macro for the first time(!).
I read the macro chapter of Rust by Example, and failed for a while.
Then I tried reading the actual reference which I feel that got me a few steps further but I still couldn't get it right.
Then I really got into it and found this cool explanation and thought that I actually had it this time, but I still can't seem to get my macro to work properly and compile at the same time.
my latest attempt looks is this:
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) -> ())
};
}
I'll appreciate if someone could help me understand what is my macro missing? and how to fix it, if even possible? and if not is there some other technique I could/should use to reduce the repetition in my tests?
Edit:
this is the final solution I'm using, which is the correct answer provided by #finomnis, which I slightly modified to support variadic arguments in the format! expression
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!() doesn't work, because it generates the code at compiletime and therefore needs an actual string literal formatter.
str::replace(), however, works:
fn main() {
for f in [" {}", "{} ", " {} "] {
let inp = f.replace("{}", "foo");
println!("{:?}", inp);
}
}
" foo"
"foo "
" foo "
I don't think there is any reason why doing this at runtime is a problem, especially as your format!() call in the macro is also a runtime replacement, but nonetheless I think this is an interesting challenge to learn more about macros.
There are a couple of problems with your macro.
For one, the () case should be ([], $_:tt) instead.
But the main problem with your macro is that [$f:literal, $($fs:literal),*] does not match [""] (the case where only one literal is left) because it doesn't match the required comma. This one would match: ["",].
This can be solved by converting the $(),* into $(),+ (meaning, they have to carry at least one element) and then replacing the [] (no elements left) case with [$f:literal] (one element left). This then handles the special case where only one element is left and the comma doesn't match.
The way you select your intermediate results has minor bugs in several places. At some places, you forgot the () around it, and the arguments may be in the wrong order. Further, it's better to transport them as $(tt)* instead of $(tt),*, as the tt contains the comma already.
Your $as_expr case doesn't serve much purpose according to the newer macro book, so I would remove it.
This is how your code could look like after fixing all those things:
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_"]
However, if you use cargo expand to print what the macro resolves to, this is what you get:
#![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)],
));
};
}
What you can clearly see here is that the format! is still a runtime call. So I don't think that the macro actually creates any kind of speedup.
You could fix that with the 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]
You can now see that the type is &'static str, meaning, it is now being formatted at compile time and stored in the binary as a static string.
That all said, I think the entire recursion in the macro is quite pointless. It seems like it can be done with a single repetition:
macro_rules! map_fmt {
([$($fs:literal),*], $args:expr) => {{
[$(format!($fs, $args)),*]
}};
}
fn main() {
let fmt = map_fmt!(["_{}", "{}_", "_{}_"], "foo");
println!("{:?}", fmt);
}
["_foo", "foo_", "_foo_"]
If you want to support an arbitrary number of arguments for format!(), then you could do:
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_"]

How to use `Option<MyStruct>` on format?

I'm trying to use an Option of a Method on format!
/// Converts a method into a `&str`.
impl<'a> From<&'a Method> for &'a str {
fn from(v: &'a Method) -> Self {
match v {
Method::Describe => "DESCRIBE",
Method::GetParameter => "GET_PARAMETER",
Method::Options => "OPTIONS",
Method::Pause => "PAUSE",
Method::Play => "PLAY",
Method::PlayNotify => "PLAY_NOTIFY",
Method::Redirect => "REDIRECT",
Method::Setup => "SETUP",
Method::SetParameter => "SET_PARAMETER",
Method::Teardown => "TEARDOWN",
Method::Extension(ref v) => v,
}
}
}
How do I use a Option<Method> in format!? I don't want to use unwrap, I want it to be empty in case there's no Method
I guess it's something like this:
let method = `Some(Method::Describe);
let method = match method {
Some(method) => method.something,
None => ""
};
I believe in this case you can use map_or_else
So what you have would looks something like this?
let method = Some(Method::Describe);
let method = method.map_or_else(|| String::default(), |m| m.something);

match multiple enum types in macro

i have a macro thats used for matching a Box<dyn error::Error> with different enums. with the version im using now, i need to call it repeatedly in an if else chain for each error type i want to match against.
#[macro_export]
macro_rules! dyn_match {
($e:expr, $($pat:pat => $result:expr),*) => (
if let Some(e) = $e.downcast_ref() {
match e {
$(
$pat => {$result; true}
),*
}
} else {false}
);
}
...
//fn example(x: u32) -> Result<u32, Box<dyn error::Error>>;
match example(9) {
Ok(_) => Ok(()),
Err(e) => {
if dyn_match!(e,
ExampleError1::ThisError(2) => panic!("it was 2!"),
i => {}
) {Ok(())}
else if dyn_match!(e,
ExampleError2::ThatError(8) => panic!("it was 8!"),
ExampleError2::ThatError(9) => panic!("it was 9!"),
i => {}
) {Ok(())}
else {panic!("{}",e)}
}
}
...
my goal is to make it so i can pass multiple error types to it and it'll do an individual match for each type. the only issue is, to convert the type from Box<dyn error::Error> to its original error type i need to call downcast_ref() on it. this works fine when every error passed is of the same type, but when i attempt to call a separate match block for each supplied arm it throws the error cannot infer type for type parameter 'T' declared on the associated function 'downcast_ref'
#[macro_export]
macro_rules! new_dyn_match {
($e:expr, $($pat:pat => $result:expr),*) => (
{
$(
if let Some(e) = $e.downcast_ref() { //cannot infer type for type parameter `T` declared on the associated function `downcast_ref`
if let $pat = e {$result}
}
)*
}
);
}
...
//fn example(x: u32) -> Result<u32, Box<dyn error::Error>>;
match example(9) {
Ok(_) => Ok(()),
Err(e) => {
new_dyn_match!(e,
ExampleError1::ThisError(2) => panic!("it was 2!"),
ExampleError2::ThatError(8) => panic!("it was 8!"),
ExampleError2::ThatError(9) => panic!("it was 9!"),
i => {}
);
Ok(())
}
}
...
im new to macros but it seems like, upon expansion during compile, it should be able to see which type each match would be compared against and implicitly downcast to that type. im really not sure how to fix this. any ideas?
got it!
#[macro_export]
macro_rules! dynmatch {
($e:expr, $(type $ty:ty {$(arm $pat:pat => $result:expr),*, _ => $any:expr}),*, _ => $end:expr) => (
$(
if let Some(e) = $e.downcast_ref::<$ty>() {
match e {
$(
$pat => {$result}
)*
_ => $any
}
} else
)*
{$end}
);
}
...
let _i = match example(3) {
Ok(i) => i,
Err(e) => {
dynmatch!(e, //the dynamic error to be matched
type ExampleError1 { //an error group
arm ExampleError1::ThisError(2) => panic!("it was 2!"), //arm [pattern] => {code}
_ => panic!("{}",e) //_ => {code}
},
type ExampleError2 {
arm ExampleError2::ThatError(8) => panic!("it was 8!"),
arm ExampleError2::ThatError(9) => 9,
_ => panic!("{}",e)
},
_ => panic!("{}",e) //what to do if error group isn't found
)
}
};
...
credit to this crate for the solution.
also, if anyone has any ideas on a better solution please let me know!

How can I get the T from an Option<T> when using syn?

I'm using syn to parse Rust code. When I read a named field's type using field.ty, I get a syn::Type. When I print it using quote!{#ty}.to_string() I get "Option<String>".
How can I get just "String"? I want to use #ty in quote! to print "String" instead of "Option<String>".
I want to generate code like:
impl Foo {
pub set_bar(&mut self, v: String) {
self.bar = Some(v);
}
}
starting from
struct Foo {
bar: Option<String>
}
My attempt:
let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
let data: Data = ast.data;
match data {
Data::Struct(ref data) => match data.fields {
Fields::Named(ref fields) => {
fields.named.iter().for_each(|field| {
let name = &field.ident.clone().unwrap();
let ty = &field.ty;
quote!{
impl Foo {
pub set_bar(&mut self, v: #ty) {
self.bar = Some(v);
}
}
};
});
}
_ => {}
},
_ => panic!("You can derive it only from struct"),
}
My updated version of the response from #Boiethios, tested and used in a public crate, with support of several syntaxes for Option:
Option
std::option::Option
::std::option::Option
core::option::Option
::core::option::Option
fn extract_type_from_option(ty: &syn::Type) -> Option<&syn::Type> {
use syn::{GenericArgument, Path, PathArguments, PathSegment};
fn extract_type_path(ty: &syn::Type) -> Option<&Path> {
match *ty {
syn::Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
_ => None,
}
}
// TODO store (with lazy static) the vec of string
// TODO maybe optimization, reverse the order of segments
fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
let idents_of_path = path
.segments
.iter()
.into_iter()
.fold(String::new(), |mut acc, v| {
acc.push_str(&v.ident.to_string());
acc.push('|');
acc
});
vec!["Option|", "std|option|Option|", "core|option|Option|"]
.into_iter()
.find(|s| &idents_of_path == *s)
.and_then(|_| path.segments.last())
}
extract_type_path(ty)
.and_then(|path| extract_option_segment(path))
.and_then(|path_seg| {
let type_params = &path_seg.arguments;
// It should have only on angle-bracketed param ("<String>"):
match *type_params {
PathArguments::AngleBracketed(ref params) => params.args.first(),
_ => None,
}
})
.and_then(|generic_arg| match *generic_arg {
GenericArgument::Type(ref ty) => Some(ty),
_ => None,
})
}
You should do something like this untested example:
use syn::{GenericArgument, PathArguments, Type};
fn extract_type_from_option(ty: &Type) -> Type {
fn path_is_option(path: &Path) -> bool {
leading_colon.is_none()
&& path.segments.len() == 1
&& path.segments.iter().next().unwrap().ident == "Option"
}
match ty {
Type::Path(typepath) if typepath.qself.is_none() && path_is_option(typepath.path) => {
// Get the first segment of the path (there is only one, in fact: "Option"):
let type_params = typepath.path.segments.iter().first().unwrap().arguments;
// It should have only on angle-bracketed param ("<String>"):
let generic_arg = match type_params {
PathArguments::AngleBracketed(params) => params.args.iter().first().unwrap(),
_ => panic!("TODO: error handling"),
};
// This argument must be a type:
match generic_arg {
GenericArgument::Type(ty) => ty,
_ => panic!("TODO: error handling"),
}
}
_ => panic!("TODO: error handling"),
}
}
There's not many things to explain, it just "unrolls" the diverse components of a type:
Type -> TypePath -> Path -> PathSegment -> PathArguments -> AngleBracketedGenericArguments -> GenericArgument -> Type.
If there is an easier way to do that, I would be happy to know it.
Note that since syn is a parser, it works with tokens. You cannot know for sure that this is an Option. The user could, for example, type std::option::Option, or write type MaybeString = std::option::Option<String>;. You cannot handle those arbitrary names.

How to write macro for similar matching arms?

I have some repetitive code
match *x {
A(ref a) => "special",
B(ref a) => "B foo",
C(ref a) => "C foo",
D(ref a) => "D foo",
// ...
}
I would like a macro like
macro_rules! generic_fmt {
($T:ident) => {
$T(ref a) => {"$T foo"},
}
}
So that I can simplify my matching to
match *x {
A(ref a) => "special",
generic_fmt!(B),
generic_fmt!(C),
generic_fmt!(D),
// ...
}
What's the best way to do that? I'm using rustc 1.19.0-nightly.
You cannot do that exactly. A macro cannot expand to a match arm (<Variant> => <Expression>).
The closest you could get is probably something like this:
enum Foo {
A(u32),
B(u32),
C(u32),
D(u32),
}
macro_rules! gen1 {
($Variant:ident) => {
Foo::$Variant(ref a)
}
}
macro_rules! gen2 {
($Variant:ident) => {
concat!(stringify!($Variant), " foo")
}
}
fn print(x: Foo) {
println!("{}", match x {
Foo::A(ref a) => "special",
gen1!(B) => gen2!(B),
gen1!(C) => gen2!(C),
gen1!(D) => gen2!(D),
});
}
fn main() {
print(Foo::A(42));
print(Foo::B(42));
print(Foo::C(42));
print(Foo::D(42));
}
Playground link.
If changing your output a little is acceptable, you could use a catch-all for all the same arms:
match x {
Foo::A(ref a) => println!("special"),
_ => println!("{:?} Foo", x),
}
Playground link
But this would print the type and its parameters. If you are on nightly, and not afraid of experimental, you can use std::intrinsics::type_name to display only the type name.
Or you can use a macro that does all your match arms:
macro_rules! gen_match {
($x:ident, $Special:ident, [$( $Foo:ident ),*]) => {
match $x {
Foo::$Special(ref a) => println!("special"),
$(Foo::$Foo(ref a) => println!("{} foo", stringify!($Foo)),)*
}
}
}
and calling it:
gen_match!(x, A, [B, C, D]);
Playground link
The brackets around the variants that will be generic formatted are for readability, they could be removed from macro definition

Resources